Skip to content

feat: add MACHINE_SCREENING category to AMD for carrier call-screening services #1374

@karan-dhir

Description

@karan-dhir

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:

  1. Add MACHINE_SCREENING to the AMDCategory enum (with JSDoc explaining the contract).
  2. Add MACHINE_RESULT_CATEGORIES set + isMachineResult helper next to the existing MACHINE_CATEGORIES / isMachineCategory. Doc-comments explain the asymmetry.
  3. Update both isMachine: isMachineCategory(category) callsites to use isMachineResult instead.
  4. Update the auto-interrupt gate at finish() to use isMachineCategory(result.category) directly (was result.isMachine) so screening doesn't auto-interrupt.
  5. 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!

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions