Skip to content

feat(amd): add MACHINE_SCREENING category for carrier call-screening services#1386

Open
karan-dhir wants to merge 1 commit into
livekit:mainfrom
karan-dhir:add-machine-screening-amd-category
Open

feat(amd): add MACHINE_SCREENING category for carrier call-screening services#1386
karan-dhir wants to merge 1 commit into
livekit:mainfrom
karan-dhir:add-machine-screening-amd-category

Conversation

@karan-dhir
Copy link
Copy Markdown

Closes #1374 (issue filed first per CONTRIBUTING.md; opening PR for review now that the proposal has been written up there).

Summary

Adds a 6th AMDCategory for carrier-injected call-screening prompts (Google Pixel Call Screen, iOS 18 Call Screening, and similar TTS services that intercept the call and ask the caller to identify themselves before reaching the human owner).

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. Distinguishing them at the AMD layer lets consumers respond appropriately (e.g. play a short identification greeting in response, instead of a voicemail-message script).

Motivated by an outbound voice agent (Woflow) where Pixel Call Screen prompts were classifying as voicemail and the agent was dumping a full voicemail script into Pixel's screened transcript shown to the recipient. See #1374 for the full motivation.

Surface contract

export enum AMDCategory {
  HUMAN = 'human',
  MACHINE_IVR = 'machine-ivr',
  MACHINE_VM = 'machine-vm',
  MACHINE_UNAVAILABLE = 'machine-unavailable',
  MACHINE_SCREENING = 'machine-screening',  // NEW
  UNCERTAIN = 'uncertain',
}

For MACHINE_SCREENING:

Field Behaviour
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.

Auto-interrupt at finish() now gates on isMachineCategory(result.category) directly instead of result.isMachine, preserving the asymmetry without making the field-level contract surprising.

Implementation

4 changes in agents/src/voice/amd.ts + 1 new test in agents/src/voice/amd.test.ts:

  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.
  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 both isMachine: true AND session.interrupt not called when interruptOnMachine: true.

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 whose model isn't returning MACHINE_SCREENING from the LLM yet (because they have a custom prompt, or are running on an older model) get the same behaviour as before.

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. Happy to mirror this change there once the JS direction is settled — let me know which order you'd prefer.

Diff stats

+77 / -4 LOC across 2 files. Single commit.

Notes for reviewers

  • Naming: MACHINE_SCREENING was chosen for parallelism with the existing MACHINE_* prefix and the fact that screening is technically a machine (TTS) intercepting the call. Open to alternatives if there's a project convention I'm missing.
  • The asymmetry between isMachine (true) and interruptOnMachine (no interrupt) was deliberate; happy to switch to either:
    • isMachine: false for screening (consumers' machine-checks ignore screening) + auto-interrupt off — simpler but less informative
    • isMachine: true + auto-interrupt on — matches existing pattern but breaks the screening use case (the greeting gets cancelled)
    • Current: isMachine: true + auto-interrupt off — informative + functional
  • CLA: I'll sign on first interaction with the bot.

Thanks for the review!

Adds a 6th `AMDCategory` for carrier-injected call-screening prompts
(Google Pixel Call Screen, iOS 18 Call Screening, and similar). Today
these prompts are most often classified 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.

Surface contract:

  - `result.category === MACHINE_SCREENING` on detection.
  - `result.isMachine === true` so consumers' "did a machine answer?"
    checks behave intuitively.
  - `interruptOnMachine: true` does NOT auto-interrupt on screening.
    Callers handling screening typically need to play a short
    identification greeting in response, which an automatic interrupt
    would cancel. Implementation note: auto-interrupt now gates on
    `isMachineCategory(result.category)` (the narrower set excluding
    SCREENING), while `result.isMachine` uses the wider
    `isMachineResult` set including SCREENING. This is the intended
    asymmetry.

Documented in the enum JSDoc + with examples in the LLM classification
prompt (`AMD_PROMPT`).

Test: new case asserts SCREENING → `isMachine: true` AND `interrupt`
NOT called when `interruptOnMachine: true`. Existing 4 test cases
unchanged. Vitest, lint, and tsc all clean on the changed files.

Motivated by an outbound voice agent (Woflow) where Pixel Call Screen
prompts were being classified as voicemail and the agent dumped a full
voicemail script into Pixel's screened transcript shown to the
recipient. With MACHINE_SCREENING as a distinct verdict, callers can
respond appropriately without false-flagging as voicemail.
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 4, 2026

⚠️ No Changeset found

Latest commit: bd19a1a

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 3 additional findings.

Open in Devin Review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

1 participant