Collect required details in any order with a voice AI agent
AI Agentsslot filling for voice AI agents
Let a caller give the required details of a report in any order, keep the checklist in the tool handlers, and refuse to file until every required detail is in.
Also called form filling by voice, absence reporting line, intake checklist, required fields before submit
tool-callinggovernance
The claim
A form has required fields, and a phone call does not fill them in order. The caller says why they are out before they say who they are. This agent takes the details as they come. record_detail takes a field name and a value and writes the whole details object back with set_global_data. Its response tells the model what is still needed, in the words the checklist uses, so the model asks for that and nothing already recorded.
Why it holds
submit_report takes no arguments. It reads the collected details from the global_data the platform posted with the call, and it files the report only when every required field is there. A model that asks to submit early gets a MISSING list, not a report. The prompt says “record each detail as soon as you hear it”; the handlers are what make the order not matter.
How it works
REQUIRED = {"employee_name": "the employee's name",
"shift_date": "the date of the shift",
"reason": "the reason for the absence"}
def record_detail(self, args, raw_data):
field, value = _text(args.get("field")), _text(args.get("value"))
details = dict((raw_data.get("global_data") or {}).get("details") or {})
details[field] = value
result = FunctionResult(f"Recorded {FIELDS[field]}. {status_line(details)}")
result.update_global_data({"details": details}) # the whole object
return result
def submit_report(self, args, raw_data):
details = (raw_data.get("global_data") or {}).get("details") or {}
if missing(details):
return FunctionResult("MISSING: " + ", ".join(...) + ". Ask for those.")
...
The field parameter is an enum of the checklist, so the model is offered the field names rather than inventing them. The enum shapes the offer only: an out-of-enum value still reaches the handler, which answers UNKNOWN_FIELD and writes nothing.
set_global_data merges top-level keys. Writing {"details": {"reason": ...}} would replace the whole object and lose the fields already recorded. So the handler reads the current object from the request, adds the one field, and writes the whole object back. The verifier threads global_data between calls the way the platform does, applying each action before the next call.
submit_report also reads report_id from global_data. Once a report is filed, asking again, or a retry of the same call, gets ALREADY_FILED with the reference and no second report.
What the first record_detail returns, with the reason given first:
{"response": "Recorded the reason for the absence. Still needed: the employee's name, the date of the shift.",
"action": [{"set_global_data": {"details": {"reason": "a fever"}}}]}
Limitations
The report is filed into a list in the process. Your HR system replaces it. The report_id check makes a retry within the call harmless; a retry after the call has ended is your system’s idempotency key to keep.
Two record_detail calls emitted in one model turn would each read the same global_data, and the second whole-object write would drop the first field. Whether the platform applies one action before the next call in the same turn is not something this verifier can prove. The prompt asks for one detail per call for that reason.
Optional fields are recorded the same way but never block the submit. Whether a detail is required is a line in REQUIRED, not a prompt sentence.
The TypeScript SDK’s /swaig route in 2.0.5 hands argument to the handler as posted, while the platform nests the arguments in argument.parsed[0]. The surface overrides onFunctionCall to unwrap that shape.
What to change first
Move reason from REQUIRED to OPTIONAL and run the verifier. The first submit still refuses, but for one field, and the expected MISSING line no longer matches. The checklist is code, and the verifier reads it as code.