The first agent finds the patient and a free time, then calls handoff. Your server returns agent and a new prompt. Rights stay the ones from the route. The new tool list is bound for the next dispatch. The live session that is already open is not given a replacement tool schema, so it keeps calling the tools from the start of the call.
1. Records in DotSIP
- Agents agent-desk and agent-confirm. agent-desk tools: find_patient, free_slots, handoff. Router tool-router-builtin. Rights on the route: right-1.
- All three tools use endpoint http://127.0.0.1:9099/tool and rights right-1.
- agent-confirm may list other tools in the admin. This call does not push that list into the open voice session.
- route-tools URL http://127.0.0.1:9099/route.
- Under SIP accounts, the trunk that receives the call. Set its inbound route to route-tools, or leave that field empty so the call uses route-tools.
2. Your database
CREATE TABLE handoff (id TEXT PRIMARY KEY, next_agent TEXT NOT NULL, prompt TEXT NOT NULL);
INSERT INTO handoff(id, next_agent, prompt) VALUES
('desk', 'agent-confirm', 'Confirm 28 Sep at 09:20 for patient p-17. Do not book anything else.');
3. What DotSIP posts for handoff
{
"format": "dotsip.tool-call",
"format_version": 1,
"call_id": "c-7f3a",
"tool": "handoff",
"arguments": { "day": "2026-09-28", "time": "09:20", "patient_id": "p-17" },
"caller": { "number": "+421901234567", "name": null, "rights": "right-1" }
}
4. The server
The route starts agent-desk. find_patient and free_slots read the same tables as the appointment example. handoff reads the handoff row and returns agent plus prompt. Do not send rights. It is ignored.
<?php
$db = new PDO("sqlite:" . __DIR__ . "/operator.sqlite");
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$db->exec("CREATE TABLE IF NOT EXISTS handoff (id TEXT PRIMARY KEY, next_agent TEXT NOT NULL, prompt TEXT NOT NULL)");
$db->exec("INSERT OR IGNORE INTO handoff(id, next_agent, prompt) VALUES ('desk', 'agent-confirm', 'Confirm 28 Sep at 09:20 for patient p-17. Do not book anything else.')");
$body = json_decode(file_get_contents("php://input"), true);
header("Content-Type: application/json");
$format = is_array($body) && isset($body["format"]) ? $body["format"] : "";
if ($format === "dotsip.route") {
echo json_encode(array("format" => "dotsip.route-result", "format_version" => 2, "commands" => array(
array("do" => "answer"),
array("do" => "agent", "agent" => "agent-desk", "rights" => "right-1", "prompt" => "Find the patient and a free time. When they agree, call handoff."),
)));
return;
}
if ($format === "dotsip.tool-call" && (isset($body["tool"]) ? $body["tool"] : "") === "handoff") {
$row = $db->query("SELECT next_agent, prompt FROM handoff WHERE id = 'desk'")->fetch(PDO::FETCH_ASSOC);
echo json_encode(array(
"ok" => true,
"result" => array("day" => "2026-09-28", "time" => "09:20"),
"prompt" => $row["prompt"],
"agent" => $row["next_agent"],
));
return;
}
if ($format === "dotsip.tool-call") {
echo json_encode(array("ok" => true, "result" => array("patient_id" => "p-17", "slots" => array("09:20"))));
return;
}
http_response_code(400);
echo json_encode(array("ok" => false, "error" => "bad request"));
import sqlite3
from flask import Flask, request, jsonify
app = Flask(__name__)
con = sqlite3.connect("operator.sqlite", check_same_thread=False)
con.execute("CREATE TABLE IF NOT EXISTS handoff (id TEXT PRIMARY KEY, next_agent TEXT NOT NULL, prompt TEXT NOT NULL)")
con.execute("INSERT OR IGNORE INTO handoff(id, next_agent, prompt) VALUES ('desk', 'agent-confirm', 'Confirm 28 Sep at 09:20 for patient p-17. Do not book anything else.')")
con.commit()
@app.post("/route")
def route():
body = request.get_json(silent=True) or {}
if body.get("format") != "dotsip.route":
return jsonify({"error": "bad request"}), 400
return jsonify({"format": "dotsip.route-result", "format_version": 2, "commands": [
{"do": "answer"},
{"do": "agent", "agent": "agent-desk", "rights": "right-1", "prompt": "Find the patient and a free time. When they agree, call handoff."},
]})
@app.post("/tool")
def tool():
body = request.get_json(silent=True) or {}
if body.get("format") != "dotsip.tool-call":
return jsonify({"ok": False, "error": "bad request"}), 400
if body.get("tool") != "handoff":
return jsonify({"ok": True, "result": {"patient_id": "p-17", "slots": ["09:20"]}})
row = con.execute("SELECT next_agent, prompt FROM handoff WHERE id = ?", ("desk",)).fetchone()
return jsonify({
"ok": True,
"result": {"day": "2026-09-28", "time": "09:20"},
"prompt": row[1], "agent": row[0],
})
const http = require("http");
const { DatabaseSync } = require("node:sqlite");
const db = new DatabaseSync("operator.sqlite");
db.exec("CREATE TABLE IF NOT EXISTS handoff (id TEXT PRIMARY KEY, next_agent TEXT NOT NULL, prompt TEXT NOT NULL)");
db.exec("INSERT OR IGNORE INTO handoff(id, next_agent, prompt) VALUES ('desk', 'agent-confirm', 'Confirm 28 Sep at 09:20 for patient p-17. Do not book anything else.')");
const rowOf = db.prepare("SELECT next_agent, prompt FROM handoff WHERE id = ?");
function send(res, payload) {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify(payload));
}
http.createServer((req, res) => {
let raw = "";
req.on("data", (c) => { raw += c; });
req.on("end", () => {
let body = {};
try { body = JSON.parse(raw || "{}"); } catch (e) { body = {}; }
if (req.url === "/route" && body.format === "dotsip.route") {
send(res, { format: "dotsip.route-result", format_version: 2, commands: [
{ do: "answer" },
{ do: "agent", agent: "agent-desk", rights: "right-1", prompt: "Find the patient and a free time. When they agree, call handoff." }
]});
return;
}
if (body.format === "dotsip.tool-call" && body.tool === "handoff") {
const row = rowOf.get("desk");
send(res, { ok: true, result: { day: "2026-09-28", time: "09:20" }, prompt: row.prompt, agent: row.next_agent });
return;
}
if (body.format === "dotsip.tool-call") {
send(res, { ok: true, result: { patient_id: "p-17", slots: ["09:20"] } });
return;
}
res.writeHead(400, { "Content-Type": "application/json" });
res.end(JSON.stringify({ ok: false, error: "bad request" }));
});
}).listen(9099);
After handoff, the open voice session still offers find_patient, free_slots, and handoff. It does not receive a new tool schema. The new prompt is the text from the handoff table.