Summary
Following the "open an issue first" guidance in CONTRIBUTING.md: proposing a 6th AMDCategory for carrier-injected call-screening prompts (Google Pixel Call Screen, iOS 18 Call Screening, and similar).
A working implementation lives on karan-dhir:add-machine-screening-amd-category for review. Happy to open a PR if maintainers agree on the approach (or to revise based on feedback).
Motivation
Carrier-side screening services intercept outbound calls and play a TTS prompt asking the caller to identify themselves before reaching the human owner. Examples:
- Google Pixel Call Screen: "If you record your name and reason for calling, I'll see if this person is available."
- iOS 18 Call Screening: "Please state your name and the reason for calling."
- Generic carrier announcement: "This call may be screened."
Today these prompts most often classify as MACHINE_VM because they sound like a TTS message after pickup — but they're not voicemail. The callee is reachable; the caller is being asked to record a brief identification.
In our outbound voice agent, this caused the agent to deliver its full voicemail script into Pixel's screened transcript shown to the recipient, who then dismissed the call. Distinguishing screening from voicemail at the AMD layer would let consumers respond appropriately (e.g. play a short identification greeting instead of a voicemail-message script).
Proposed surface
export enum AMDCategory {
HUMAN = 'human',
MACHINE_IVR = 'machine-ivr',
MACHINE_VM = 'machine-vm',
MACHINE_UNAVAILABLE = 'machine-unavailable',
MACHINE_SCREENING = 'machine-screening', // NEW
UNCERTAIN = 'uncertain',
}
Contract
| Field |
Behaviour for MACHINE_SCREENING |
result.category |
'machine-screening' |
result.isMachine |
true (consumers' "did a machine answer?" checks behave intuitively) |
Auto-interrupt with interruptOnMachine: true |
NOT triggered (callers handling screening typically need to play a short identification greeting in response, which an automatic interrupt would cancel) |
The asymmetry between result.isMachine and interruptOnMachine is implemented by splitting the existing MACHINE_CATEGORIES set into two:
MACHINE_CATEGORIES — drives auto-interrupt. Excludes MACHINE_SCREENING.
MACHINE_RESULT_CATEGORIES — drives result.isMachine. Includes MACHINE_SCREENING.
Risk / breakage
- Additive only. Existing 5 categories continue to work identically.
- The
interruptOnMachine semantics for HUMAN / IVR / VM / UNAVAILABLE / UNCERTAIN are unchanged byte-for-byte.
- Consumers who never see
MACHINE_SCREENING from the LLM (because their model isn't tuned for it yet) get the same behaviour as before.
Implementation notes
The branch makes 4 changes in agents/src/voice/amd.ts + 1 new test:
- Add
MACHINE_SCREENING to the AMDCategory enum (with JSDoc explaining the contract).
- Add
MACHINE_RESULT_CATEGORIES set + isMachineResult helper next to the existing MACHINE_CATEGORIES / isMachineCategory. Doc-comments explain the asymmetry.
- Update both
isMachine: isMachineCategory(category) callsites to use isMachineResult instead.
- Update the auto-interrupt gate at
finish() to use isMachineCategory(result.category) directly (was result.isMachine) so screening doesn't auto-interrupt.
- Update
AMD_PROMPT with the new category description + 4 example prompts (Pixel + iOS + generic).
Test added: should classify call screening as machine without auto-interrupt — asserts isMachine: true AND session.interrupt not called when interruptOnMachine: true.
Verification done locally
- ✅
pnpm exec vitest run agents/src/voice/amd.test.ts — 5/5 pass (4 existing + 1 new)
- ✅
pnpm exec tsc --noEmit -p agents/tsconfig.json — clean
- ✅
pnpm format:write — clean (no diff after format)
- ✅
pnpm lint --filter=@livekit/agents — zero warnings on changed files (pre-existing 120 warnings on other files unaffected)
Python parity
The JS classifier doc-comments reference python classifier.py patterns, suggesting a sibling implementation in livekit/agents. If accepted here, I'd happily mirror the change there too — let me know whether you'd prefer the JS or Python PR to land first.
Branch ready for review
karan-dhir/agents-js@add-machine-screening-amd-category — single commit, +77 / −4 LOC.
Happy to:
- Open the PR if you'd like to proceed
- Adjust naming (
MACHINE_SCREENING vs alternatives like MACHINE_GATEKEEPER)
- Adjust the asymmetry pattern if you'd prefer a different
interruptOnMachine semantic
- Coordinate the Python parity PR
Thanks for considering!
Summary
Following the "open an issue first" guidance in CONTRIBUTING.md: proposing a 6th
AMDCategoryfor carrier-injected call-screening prompts (Google Pixel Call Screen, iOS 18 Call Screening, and similar).A working implementation lives on
karan-dhir:add-machine-screening-amd-categoryfor review. Happy to open a PR if maintainers agree on the approach (or to revise based on feedback).Motivation
Carrier-side screening services intercept outbound calls and play a TTS prompt asking the caller to identify themselves before reaching the human owner. Examples:
Today these prompts most often classify as
MACHINE_VMbecause they sound like a TTS message after pickup — but they're not voicemail. The callee is reachable; the caller is being asked to record a brief identification.In our outbound voice agent, this caused the agent to deliver its full voicemail script into Pixel's screened transcript shown to the recipient, who then dismissed the call. Distinguishing screening from voicemail at the AMD layer would let consumers respond appropriately (e.g. play a short identification greeting instead of a voicemail-message script).
Proposed surface
Contract
MACHINE_SCREENINGresult.category'machine-screening'result.isMachinetrue(consumers' "did a machine answer?" checks behave intuitively)interruptOnMachine: trueThe asymmetry between
result.isMachineandinterruptOnMachineis implemented by splitting the existingMACHINE_CATEGORIESset into two:MACHINE_CATEGORIES— drives auto-interrupt. ExcludesMACHINE_SCREENING.MACHINE_RESULT_CATEGORIES— drivesresult.isMachine. IncludesMACHINE_SCREENING.Risk / breakage
interruptOnMachinesemantics for HUMAN / IVR / VM / UNAVAILABLE / UNCERTAIN are unchanged byte-for-byte.MACHINE_SCREENINGfrom the LLM (because their model isn't tuned for it yet) get the same behaviour as before.Implementation notes
The branch makes 4 changes in
agents/src/voice/amd.ts+ 1 new test:MACHINE_SCREENINGto theAMDCategoryenum (with JSDoc explaining the contract).MACHINE_RESULT_CATEGORIESset +isMachineResulthelper next to the existingMACHINE_CATEGORIES/isMachineCategory. Doc-comments explain the asymmetry.isMachine: isMachineCategory(category)callsites to useisMachineResultinstead.finish()to useisMachineCategory(result.category)directly (wasresult.isMachine) so screening doesn't auto-interrupt.AMD_PROMPTwith the new category description + 4 example prompts (Pixel + iOS + generic).Test added:
should classify call screening as machine without auto-interrupt— assertsisMachine: trueANDsession.interruptnot called wheninterruptOnMachine: true.Verification done locally
pnpm exec vitest run agents/src/voice/amd.test.ts— 5/5 pass (4 existing + 1 new)pnpm exec tsc --noEmit -p agents/tsconfig.json— cleanpnpm format:write— clean (no diff after format)pnpm lint --filter=@livekit/agents— zero warnings on changed files (pre-existing 120 warnings on other files unaffected)Python parity
The JS classifier doc-comments reference
python classifier.pypatterns, suggesting a sibling implementation inlivekit/agents. If accepted here, I'd happily mirror the change there too — let me know whether you'd prefer the JS or Python PR to land first.Branch ready for review
karan-dhir/agents-js@add-machine-screening-amd-category— single commit, +77 / −4 LOC.Happy to:
MACHINE_SCREENINGvs alternatives likeMACHINE_GATEKEEPER)interruptOnMachinesemanticThanks for considering!