← Docs hub

DeviceAgent ↔ AI-Agent 구현 요청서

2026-09-29 구현 범위: 화자 등록·상태 전달·목록·삭제·서버 관심사 조회가 대상이다. 대화내역 저장·조회·삭제·용량 조회·실시간 저장 알림은 다음 차수로 제외했으며 현재 공개 인터페이스에 없다. 발음 보조 값도 외부에서 받거나 보내지 않고 AI-Agent 내부에서만 처리한다.

대상: SoC DeviceAgent 담당과 AI-Agent Service 담당. 이 문서의 구현 범위는 기기 내부 Binder 경계다. APP·Backend·IoTAgent의 외부 통신 설계는 각 담당이 협의하며, 이 문서에서 토픽이나 Backend API를 지정하지 않는다.

담당별 범위 · 등록 App 경계

상세 연동 보강: 10~15절에서 누락값·자료형, 호출자별 반환값, 관리 결과의 필드 조건, Bundle 왕복 예시, 오류·경합 시험을 확인한다. 표의 송신 규칙과 현재 수신 코드의 검사 범위는 구분한다.

1. 책임과 연결

항목 DeviceAgent AI-Agent
등록 제어 검증 후 등록 제어 Bundle 전달 녹음·검사·생성·취소 실행
상태 수신 상태를 등록 App과 외부 통신 측에 중계 상태·순번·진행률·종료 여부 생성
프로필 임베딩과 식별 정보를 인수하고 결과 반환 voiceId·대표 임베딩·닉네임 생성/보관
관리 목록·삭제 요청과 결과 중계 로컬 프로필 관리
관심사 조회를 외부로 연결하고 결과 반환 인식된 voiceId로 조회. 서버 원본을 대체하지 않음

AI-Agent 패키지는 com.skmagic.ondeviceai.agent. callback은 CALLER_LLM=1이다. CALLER_ONDEVICE=7은 별도 앱 항목이므로 이 경계에 사용하지 않는다.

방향 실제 호출 형태 결과 전달
AI-Agent → DeviceAgent sendModuleCommand(input: Bundle, output: Bundle): Int 즉시 값은 접수/거절; 업무 결과는 별도 callback
DeviceAgent → AI-Agent onModuleCallbackLoop(CALLER_LLM, Bundle) → onModuleCallback() AI-Agent가 method별로 처리

SoC는 ModuleCmdCallbackPolicy의 AI-Agent 수신 목록에 등록 제어·관리 요청·voiceProfileCommitResult·voicePersonalizationResult를 상수로 등록한다. UID/패키지 검사와 callback 대상은 서로 다른 검사이며 숫자 1만으로 호출 권한을 증명하지 않는다.

2. 공통 값과 전달 규칙

필드 Android 타입 정의
method String 아래 표의 정확한 메서드명
contractVersion String 등록/프로필: voice-profile-v2, 구형 관심사 호환: voice-memory-v1, 개인화 조회: voice-personalization-v1
requestId String 요청과 결과 연결값. 전달 중 바꾸지 않음
voiceId String 생성된 화자 프로필 키. 필요한 요청에만 포함

숫자를 문자열로 바꾸지 않는다. 등록 요청의 userId는 기존 클라이언트 호환 입력이며 실제 프로필 키를 대체하지 않는다. 원본 PCM/WAV는 이 경계로 전달하지 않는다.

3. DeviceAgent가 AI-Agent로 전달할 요청

등록 제어는 voice-profile-v2이며 공통 필드와 UI가 보낸 issuedAtMs:Long을 보존한다.

method 추가 입력 AI-Agent 동작 / 받게 되는 결과
getVoiceProfileEnrollmentState 없음 현재 상태 callback
startVoiceProfileEnrollment nickname:String, locale:String; 기존 클라이언트는 userId:String도 전송 녹음 시작. 같은 활성/보존 세션의 requestId는 현재 상태만 반환
retryVoiceProfileSet setIndex:Int 현재 재시도 가능한 세트 처리. 임의 세트 선택 API 아님
pauseVoiceProfileEnrollmentForExit setIndex:Int 나가기 확인 중 녹음 일시 정리. 별도 pause ACK 없음
cancelVoiceProfileEnrollment reason:String 취소 처리와 상태 callback
setVoiceProfilePersonalization enabled:Boolean 개인화 선택 후 프로필 생성. 관심사 목록 저장이 아님
retryVoiceProfileCreation 없음 재시도 가능한 생성 실패 단계에서 재생성
retryVoiceProfileDelivery 없음 기존 pending 프로필 재전달
closeVoiceProfileRegistration 없음 등록 UI 종료 처리. 완료 프로필 삭제 아님

외부 시작 요청은 현재 DeviceAgent에서 등록 화면을 여는 경로다. 등록 App의 시작 버튼 요청을 AI-Agent로 전달하는 경로와 합치지 않는다. 화면 실행 계약은 등록 App 문서에 있다.

관리 method 버전 추가 입력 결과 method
listVoiceProfiles voice-profile-v2 없음 voiceProfileListResult
deleteVoiceProfile voice-profile-v2 voiceId:String voiceProfileDeleteResult

현재 DeviceAgent는 관리 요청을 IoTAgent 호출자에게서 수신해 중계한다. 외부 원천이 APP인지 관제인지는 이 Binder 경계의 구분값이 아니다. 구형 set/getVoiceProfileInterests는 신규 구현 대상으로 사용하지 않는다.

4. AI-Agent 상태를 복사 전달

method=voiceProfileStateChanged, contractVersion=voice-profile-v2. AI-Agent가 생성하고 DeviceAgent는 등록 App(8)과 IoTAgent(2)에 복사 전달한다. IoTAgent 이후 전달 방법은 외부 담당 범위다.

필드 타입 값·의미
requestId String 등록 요청 ID
stateSequence Long 0 이상 상태 순번. 같은 세션에서 사용
state String 아래 상태 enum 이름
setIndex / setCount 각각 Int 1부터 시작; setIndex ≤ setCount
progress Int 0~100
reasonCode String 빈 문자열 또는 오류·대기 사유
terminal Boolean 해당 등록 처리 종료 여부
nickname String 표시 이름. 초기에는 빈 값 가능
voiceId String 생성 후 포함. COMPLETED에서는 유효한 값

상태값: WAITING_START, RECORDING_SET, SET_ACCEPTED, SET_REJECTED, PERSONALIZATION_SETTINGS, CREATING_LOCAL_PROFILE, PENDING_DEVICE_DELIVERY, COMPLETED, PROFILE_FAILED, DELIVERY_FAILED, CANCELLED.

DeviceAgent는 타입·상태값·범위를 검사하고 전달한다. success를 새로 만들어 상태를 대체하거나, 접수만으로 COMPLETED를 생성하지 않는다. 프로세스 재시작 이후 순번 연속성은 현재 보장하지 않는다.

5. 프로필 인수와 결과 반환

AI-Agent → DeviceAgent: commitVoiceProfile

필드 타입 값·의미
method / contractVersion 각각 String commitVoiceProfile / voice-profile-v2
requestId / voiceId 각각 String 원 등록 요청 / 생성된 화자 키
nickname String 표시 이름
locale String 등록 언어 메타데이터
personalizationEnabled Boolean 개인화 선택
referenceEmbedding FloatArray 대표 임베딩. putFloatArray로 전달

현재 검증은 voiceId 1~128자, 임베딩 1~4096개/유한수다. 4096은 방어 상한이며 모델 차원 규격이 아니다. DeviceAgent가 모델을 다시 실행하거나 임베딩 값을 변경하지 않는다. 임베딩과 원본 음성을 로그에 출력하지 않는다.

DeviceAgent → AI-Agent: voiceProfileCommitResult

필드 타입 값·의미
method / contractVersion 각각 String voiceProfileCommitResult / voice-profile-v2
requestId String commit과 동일
accepted Boolean DeviceAgent 전달 대기 저장 수락
cloudSynced Boolean 서버 저장 완료 여부. 시험 우회와 구분
reasonCode String 결과 사유

현재 callback에는 voiceId가 없다. 같은 requestId의 pending에 대응한다.

현재 결과 accepted cloudSynced AI-Agent 해석
INVALID_PROFILE_PAYLOAD false false 입력 검증 실패
DEVICE_OUTBOX_FAILED false false 기기 전달 대기 저장 실패
WAITING_CLOUD_SYNC true false 기기 인수만 완료. 현재 Service는 DELIVERY_FAILED
LOCAL_TEST_ACK true true 로컬 시험용 완료. 운영 서버 저장 증거 아님
callback 없음 해당 없음 해당 없음 10초 후 DEVICE_DELIVERY_TIMEOUT / DELIVERY_FAILED

제품 연동 시 남은 협의: 서버 저장 ACK를 받은 DeviceAgent가 어떤 시점에 callback을 반환할지 SoC·Backend 담당과 맞춰야 한다. 현재 소스에는 실제 Cloud ACK 전달·늦은 ACK 복구가 완성되어 있지 않다. 기존 10초 대기보다 오래 걸리는 외부 흐름은 AI-Agent 담당과 완료 시점을 별도로 합의해야 하며, 단순히 true를 반환하여 숨기지 않는다.

6. 프로필 관리 결과

관리 결과 공통: method:String, contractVersion:String, requestId:String, success:Boolean, reasonCode:String. DeviceAgent는 검증 후 IoTAgent에 전달하며 등록 App에는 보내지 않는다.

결과 성공 추가 필드
voiceProfileListResult profiles:ArrayList<Bundle>
voiceProfileDeleteResult voiceId:String

profiles 항목: voiceId·nickname은 String, personalizationEnabled는 Boolean이다. 객체 목록은 putParcelableArrayList로 전달한다.

7. 필요 시 개인화 조회

관심사 저장은 APP·Backend 담당 범위다. AI-Agent가 DeviceAgent에 get으로 요청하고 DeviceAgent가 외부 조회 결과를 돌려준다. 등록 App에는 조회 결과를 보내지 않는다.

방향 / method 필드와 타입
AI-Agent → DeviceAgent / getVoicePersonalization contractVersion:String=voice-personalization-v1, requestId:String, voiceId:String, issuedAtMs:Long, expiresAtMs:Long
DeviceAgent → AI-Agent / voicePersonalizationResult 동일 버전·requestId·voiceId, success:Boolean, reasonCode:String, expiresAtMs:Long
조회 성공 추가 필드 personalizationEnabled:Boolean, interestCodes:ArrayList<String>, revision:Long ≥ 1

AI-Agent의 요청 기한은 생성 후 5초. DeviceAgent 검증 상한은 10초이며 응답에서 기한을 연장하지 않는다. 실패 결과에는 interestCodes를 넣지 않는다. 허용 값은 WEATHER, AIR_QUALITY, HEALTH, SCHEDULE, TRAVEL, FOOD, CULTURE, SPORTS, FINANCE, TECHNOLOGY이며 최대 10개/중복 없음. 개인화 비허용이면 빈 목록이다.

Backend API나 전송 방식은 외부 담당이 정하되 위 결과 타입과 요청 대응은 유지한다. 현재 코드의 외부 중계는 Sample 경로이며 운영 연동 완료를 의미하지 않는다.

8. 결과 예시와 반환값

// DeviceAgent에서 전달 대기 저장 후 AI-Agent에 알리는 예시.
val result = Bundle().apply {
    putString("method", "voiceProfileCommitResult")
    putString("contractVersion", "voice-profile-v2")
    putString("requestId", requestId)
    putBoolean("accepted", true)
    putBoolean("cloudSynced", false)
    putString("reasonCode", "WAITING_CLOUD_SYNC")
}
// 기존 DeviceAgent의 callback 라우터에 CALLER_LLM 대상으로 전달한다.

예시는 Bundle 형식 설명이다. 실제 코드에서는 각 모듈의 Contract 상수를 사용한다. 0은 처리/중계 수락, 음수는 거절이다. 등록·상태·commit에서 호출자 거절은 -2이며, 개인화·관리에서는 -1로 반환될 수 있다. 모든 음수를 하나의 업무 오류로 변환하지 않는다. commit은 실패 반환과 callback이 함께 발생할 수 있다.

9. 구현 요청과 인수 시험

SoC DeviceAgent 구현 요청 확인할 결과
상수·callback 정책·호출자 검사 연결 AI-Agent에 요청이 한 번 전달되고 잘못된 caller는 거절
상태 Bundle 검증·복사 전달 등록 App에 같은 requestId·stateSequence·타입 도착
프로필 대기 저장과 결과 생성 저장 실패와 서버 미완료가 성공으로 바뀌지 않음
관리 결과·개인화 결과 라우팅 등록 App으로 일반 관리 결과가 유출되지 않음
외부 ACK 연동 지점 마련 운영 ACK 정책과 10초 제한의 합의·시험 결과 제시

공동 시험: 동일 start 재전달, 녹음/생성 중 취소, 취소 후 늦은 결과, 저장 실패, ACK 유실, 전달 재시도, 다른 requestId/voiceId의 조회 응답. 현재 네이티브 추론은 취소 즉시 중단이 아니라 늦은 결과를 버리는 방식이며 프로세스 강제 종료 복구까지 증명한 것은 아니다.

10. 필드별 송신 규칙과 실제 검사

송신 측은 정의된 필드를 정확한 Android 타입으로 보낸다. 기본값이 있다는 이유로 필드를 생략하지 않는다. 다음 표는 현재 코드가 무엇까지 검사하는지도 명시한다. 문서 보강으로 검증 코드가 추가된 것은 아니다.

필드·범위 송신 규칙 현재 검사·누락 시 동작
method:String 해당 요청의 정확한 이름, 필수 AI-Agent는 알려진 command/callback만 처리. 미등록 method에는 화자인식 오류 callback 보장 없음
contractVersion:String 메서드별 버전, 필수 DeviceAgent/AI-Agent 모두 버전 구분. 불일치 시 실행하지 않음
requestId:String, 등록·관리 동일 업무 요청에 같은 비어 있지 않은 값 DeviceAgent 공통 검사는 non-empty, AI-Agent는 non-blank. 모든 경로에 UUID 형식/128자 상한 검사가 있는 것은 아님
requestId / voiceId, 개인화 조회 [A-Za-z0-9_-]{1,128} DeviceAgent가 두 필드 형식 검사
voiceId:String, 삭제 [A-Za-z0-9_-]{1,128} DeviceAgent 관리 요청에서 검사
voiceId:String, commit AI-Agent가 생성한 값 그대로 commit 검사 자체는 non-empty/128자 이하. 관리 요청과 동일한 정규식 검사는 아님
nickname:String, start trim 후 1~40자, 경로 구분자·제어 문자 제외 AI-Agent 입력 계약에서 검사. DeviceAgent의 UI 내부 제어 중계는 공통 envelope 중심
locale:String ko-KR/en-US 명시 AI-Agent는 빈 값이면 ko-KR. 두 언어 allowlist 검사는 아님
enabled:Boolean 개인화 버튼 선택값 명시 누락 시 AI-Agent getBoolean 기본 true. 누락을 동의로 의도하여 사용하지 않음
issuedAtMs:Long, UI 제어 생성 시각 ms 현재 UI 클라이언트는 포함하지만 AI-Agent의 등록 제어 만료 검사에는 사용하지 않음
stateSequence:Long 현재 등록의 상태 순번 DeviceAgent는 실제 Long/0 이상 검사. Int로 보내면 거절
setIndex / setCount / progress:Int 세트 범위/0~100 DeviceAgent는 실제 Int와 범위 검사
terminal:Boolean 종료 여부 명시 DeviceAgent는 실제 Boolean 검사
referenceEmbedding:FloatArray 유한한 float 1~4096개 null·빈 배열·NaN·Infinity·상한 초과 거절
commit의 기타 메타데이터 5절 필드 전부 송신, 미설정 발음은 빈 문자열 DeviceAgent commit 검사가 nickname·locale·동의의 모든 조건을 강제하는 것은 아님

현재 모든 Bundle 키를 엄격한 스키마로 검사하는 구조는 아니다. 잘못된 타입을 넣었을 때 모든 경로에서 동일 음수 코드로 정리된다고 가정하면 안 된다. 관리 결과와 commit은 audio, wav, pcm 키를 거절하며, 관리 결과는 referenceEmbedding도 거절한다. 등록 상태를 포함한 모든 method에 동일한 민감 키 차단이 적용된 것은 아니므로 송신 측에서도 금지 데이터를 넣지 않는다.

11. 호출자·대상·즉시 반환값 행렬

아래 숫자는 DeviceAgent의 sendModuleCommand 분기 반환값이다. 반환 0이 실제 callback 수신이나 Service 실행을 보장하지 않는다. AI-Agent 자체 검증 거절은 로그만 남기고 끝날 수 있다.

입력 method 그룹 허용 송신 앱 DeviceAgent 처리·callback 대상 정상 반환 거절 반환
등록 제어 9종 등록 App Bundle 복사 → AI-Agent(1) 0 envelope -1 / 호출자 -2
원격 start·cancel IoTAgent 등록 Activity 실행 0 입력 -1 / 실행 실패 -3
voiceProfileStateChanged AI-Agent 등록 App(8), IoTAgent(2) 0 상태 envelope·타입 -1 / 호출자 -2
commitVoiceProfile AI-Agent 대기 저장 후 CommitResult → AI-Agent(1) accepted이면 0 인수 실패 -1 / 호출자 -2
목록·삭제 관리 요청 IoTAgent AI-Agent(1) 0 호출자 또는 payload -1
관리 결과 AI-Agent IoTAgent(2) 0 호출자 또는 payload -1
getVoicePersonalization AI-Agent IoTAgent(2) 0 호출자·버전·기한·타입 -1
voicePersonalizationResult IoTAgent AI-Agent(1) 0 호출자·버전·기한·타입 -1

현재 일반적인 voiceProfileCommitResult 입력을 IoTAgent가 그대로 sendModuleCommand에 넣어 AI-Agent로 전달하는 분기는 없다. 운영 서버 ACK의 수신→pending 대조→AI-Agent callback 연결은 SoC 쪽 추가 구현 지점이다. 상태나 관리 결과 분기에 넣어서 우회하지 않는다.

호출자는 Bundle 안의 caller 문자열이 아니라 Binder UID의 소속 패키지로 검사한다. 시스템 shared UID 환경에서는 이 검사만으로 앱 단위 강한 인증까지 확보했다고 볼 수 없으므로 제품 권한·서명 정책을 함께 적용한다.

12. 관리 결과의 필수 조건과 오류

모든 관리 결과에는 method, contractVersion, requestId, success, reasonCode를 보낸다. 성공의 reasonCode는 빈 문자열이고 실패에는 구체적인 코드가 필요하다. 실패 시 성공 payload가 없을 수 있으므로 success 확인 전 목록·개수 필드를 읽지 않는다.

결과 성공 payload와 의미 실패 코드/조건
voiceProfileListResult profiles:ArrayList<Bundle>. 0개는 정상 빈 목록. DeviceAgent 수락 상한 64개 PROFILE_LIST_FAILED. 현재 실패에서도 빈 profiles가 붙을 수 있음
voiceProfileDeleteResult voiceId:String. 요청한 로컬 프로필 삭제 ENROLLMENT_ACTIVE / PROFILE_NOT_FOUND / PROFILE_DELETE_FAILED

중첩 목록 필드

객체 키 타입 성공 결과의 의미
profiles[] voiceId String 등록 프로필 키
profiles[] nickname String 표시 이름
profiles[] personalizationEnabled Boolean 프로필 동의값. 이전 프로필 누락값은 false 처리

화자 삭제 성공은 서버 계정·관심사까지 원자적으로 삭제했다는 뜻이 아니다.

13. Bundle 왕복 예시

예제는 Android Bundle 구조를 보여준다. 고정 ID는 설명용이며 실제 요청에는 호출자가 생성한 ID를 사용한다. 외부 JSON이나 MQTT 형식은 지정하지 않는다.

프로필 목록 조회와 빈 목록 응답

// DeviceAgent가 AI-Agent callback(1)로 전달할 요청.
val request = Bundle().apply {
    putString("method", "listVoiceProfiles")
    putString("contractVersion", "voice-profile-v2")
    putString("requestId", "list-example-001")
}
// AI-Agent가 DeviceAgent.sendModuleCommand로 보낼 결과.
val response = Bundle().apply {
    putString("method", "voiceProfileListResult")
    putString("contractVersion", "voice-profile-v2")
    putString("requestId", "list-example-001")
    putBoolean("success", true)
    putString("reasonCode", "")
    putParcelableArrayList("profiles", arrayListOf<Bundle>())
}

빈 목록 성공과 연결 실패를 구분한다. 같은 형식으로 실제 목록을 보낼 때 각 Bundle에는 12절 profiles 필드를 넣는다.

관심사 조회 실패 응답

val requestId = "personalization-example-001"
val voiceId = "voice-example-001"
val issuedAtMs = System.currentTimeMillis()
val expiresAtMs = issuedAtMs + 5_000L
val request = Bundle().apply {
    putString("method", "getVoicePersonalization")
    putString("contractVersion", "voice-personalization-v1")
    putString("requestId", requestId)
    putString("voiceId", voiceId)
    putLong("issuedAtMs", issuedAtMs)
    putLong("expiresAtMs", expiresAtMs)
}
val failure = Bundle().apply {
    putString("method", "voicePersonalizationResult")
    putString("contractVersion", "voice-personalization-v1")
    putString("requestId", requestId)
    putString("voiceId", voiceId)
    putBoolean("success", false)
    putString("reasonCode", "BACKEND_UNAVAILABLE")
    putLong("expiresAtMs", expiresAtMs)
    // 실패에는 interestCodes를 넣지 않는다. 만료 시각도 연장하지 않는다.
}

성공 응답에서는 success=true, reasonCode="", personalizationEnabled:Boolean, revision:Long, putStringArrayList("interestCodes", arrayListOf("WEATHER"))를 사용한다. requestId·voiceId·기한은 원 요청 값 그대로다.

14. callback 순서와 재시도 한계

등록 제어 → AI-Agent 상태 callback → DeviceAgent → 등록 App
프로필 생성 → commitVoiceProfile → DeviceAgent 대기 저장
          → voiceProfileCommitResult → AI-Agent 최종/실패 상태
          → voiceProfileStateChanged → DeviceAgent → 등록 App

15. 인터페이스 인수 시험표

ID 입력/순서 기대 결과 확인 위치
AI-01 유효 listVoiceProfiles 같은 requestId의 ListResult, 빈 목록도 success=true 가능 AI-Agent → DeviceAgent → 외부 경계
AI-02 등록 상태 stateSequence를 Int로 전송 DeviceAgent -1, 등록 App에 전달하지 않음 DeviceAgent 검증
AI-03 commit 임베딩에 NaN 포함 accepted=false/cloudSynced=false, INVALID_PROFILE_PAYLOAD 및 -1 DeviceAgent 반환+callback
AI-04 기기 저장 성공, 실제 서버 ACK 없음 WAITING_CLOUD_SYNC. 완료로 바꾸지 않음 CommitResult·AI-Agent 상태
AI-05 commit callback 유실 10초 후 DELIVERY_FAILED/DEVICE_DELIVERY_TIMEOUT AI-Agent 상태
AI-06 취소한 요청에 늦은 commit 결과 현재 세션 재개·완료 없음 AI-Agent·등록 App
AI-07 같은 pending 전달 재시도 후 이전 회차 ACK 현재 구현에서 구분 불가 가능성을 확인·운영 정책 합의 SoC/Service 공동
AI-08 개인화 실패에 interestCodes 첨부 DeviceAgent -1. AI-Agent로 중계하지 않음 DeviceAgent 검증
AI-09 개인화 응답의 voiceId/requestId 변경 현재 조회 결과로 사용하지 않음 AI-Agent 조회 추적
AI-10 관리 결과에 referenceEmbedding 첨부 DeviceAgent -1, 외부 목록 결과로 전달하지 않음 DeviceAgent 검증

이 표는 시험 요구사항이며 실기기 합격 기록이 아니다. 요청 ID·method·타입·반환값·callback 순서를 남기되 음성·임베딩 원문은 로그에서 제외한다.

16. REQUEST / RESPONSE 값 예시

예시는 실제 Binder로 주고받는 Android Bundle을 Kotlin으로 구성한 코드다. 각 코드 블록은 독립된 예시이며 import android.os.Bundle을 사용한다. request는 요청 Bundle, callback은 상대편에서 구성해 돌려주는 Bundle이다. JSON 직렬화나 request/callback이라는 포장 필드는 전송하지 않는다. 필드명을 확인하기 쉽게 문자열로 표시했으며 구현에서는 기존 Contract 상수를 사용한다. 식별자·닉네임·시각은 설명용 값이다.

값 종류 Bundle에 넣는 방법
method, contractVersion, requestId, voiceId 등 문자열 putString
success, terminal, enabled 등 참/거짓 putBoolean
setIndex, setCount, progress putInt
stateSequence, issuedAtMs, expiresAtMs, revision putLong
profiles putParcelableArrayList: 내부 항목 각각 Bundle
interestCodes putStringArrayList
referenceEmbedding putFloatArray

sendModuleCommand()의 동기 반환 0은 해당 호출의 수락/중계를 뜻한다. 아래 callback과 별개이며 최종 성공을 뜻하지 않는다. callback이 동기 반환보다 먼저 도착할 수도 있다. 허용되지 않은 caller나 잘못된 필드는 중계 단계에서 거절될 수 있으므로, 모든 요청에 반드시 callback이 온다고 가정하지 않는다.

16.1 프로필 전달: 기기 저장과 서버 완료 구분

AI-Agent → DeviceAgent 요청, DeviceAgent → AI-Agent callback이다. 아래 2차원 벡터는 Bundle 구조 설명용이며 실제 모델 임베딩이나 등록 시험 데이터가 아니다. 실제로는 모델이 생성한 FloatArray 전체를 보낸다.

// REQUEST: 요청 송신 측에서 구성
val request = Bundle().apply {
    putString("method", "commitVoiceProfile")
    putString("contractVersion", "voice-profile-v2")
    putString("requestId", "enroll-001")
    putString("voiceId", "vp_demo_01")
    putString("nickname", "Niky")
    putString("locale", "ko-KR")
    putBoolean("personalizationEnabled", true)
    putFloatArray("referenceEmbedding", floatArrayOf(0.6f, 0.8f))
}

// RESPONSE: 상대편에서 구성하여 별도 callback으로 전달
val callback = Bundle().apply {
    putString("method", "voiceProfileCommitResult")
    putString("contractVersion", "voice-profile-v2")
    putString("requestId", "enroll-001")
    putBoolean("accepted", true)
    putBoolean("cloudSynced", false)
    putString("reasonCode", "WAITING_CLOUD_SYNC")
}

이 경우 기기는 인수했지만 서버 저장은 확인되지 않았다. AI-Agent는 DELIVERY_FAILED로 처리하며 등록 완료를 표시하지 않는다. callback에 voiceId를 추가해서 매칭하지 않고 pending requestId로 연결한다.

상황 accepted cloudSynced reasonCode AI-Agent 처리
기기 대기 저장 실패 false false DEVICE_OUTBOX_FAILED 전달 실패
기기 저장만 성공 true false WAITING_CLOUD_SYNC 전달 실패/재시도 가능
로컬 시험 ACK 활성화 true true LOCAL_TEST_ACK COMPLETED. 실제 서버 저장 증거 아님
10초 동안 ACK 없음 응답 없음 응답 없음 AI-Agent가 DEVICE_DELIVERY_TIMEOUT 생성 DELIVERY_FAILED

현재 실제 서버 ACK를 IoTAgent에서 DeviceAgent로 연결하는 경로는 별도 구현 대상이다. 위 로컬 시험 결과로 서버 연동 완료를 판단하지 않는다.

16.2 화자 목록 조회

DeviceAgent → AI-Agent 요청, AI-Agent → DeviceAgent 결과다. DeviceAgent는 외부 요청 경로로 결과를 돌려보내며 등록 App의 상태 callback으로 보내지 않는다.

// REQUEST: 요청 송신 측에서 구성
val request = Bundle().apply {
    putString("method", "listVoiceProfiles")
    putString("contractVersion", "voice-profile-v2")
    putString("requestId", "list-001")
}

// RESPONSE: 상대편에서 구성하여 별도 callback으로 전달
val callback = Bundle().apply {
    putString("method", "voiceProfileListResult")
    putString("contractVersion", "voice-profile-v2")
    putString("requestId", "list-001")
    putBoolean("success", true)
    putString("reasonCode", "")
    putParcelableArrayList<Bundle>("profiles", arrayListOf(
        Bundle().apply {
            putString("voiceId", "vp_demo_01")
            putString("nickname", "Niky")
            putBoolean("personalizationEnabled", true)
        }
    ))
}

등록된 화자가 없으면 success=true, profiles=[]다. 조회 실패와 빈 목록을 구분한다. 임베딩·원본 음성은 목록에 포함하지 않는다.

16.3 화자 삭제 실패

// REQUEST: 요청 송신 측에서 구성
val request = Bundle().apply {
    putString("method", "deleteVoiceProfile")
    putString("contractVersion", "voice-profile-v2")
    putString("requestId", "delete-001")
    putString("voiceId", "vp_missing")
}

// RESPONSE: 상대편에서 구성하여 별도 callback으로 전달
val callback = Bundle().apply {
    putString("method", "voiceProfileDeleteResult")
    putString("contractVersion", "voice-profile-v2")
    putString("requestId", "delete-001")
    putBoolean("success", false)
    putString("reasonCode", "PROFILE_NOT_FOUND")
    putString("voiceId", "vp_missing")
}

삭제 성공은 같은 결과 method에서 success=true, reasonCode=""이다. 이 결과는 기기 프로필 삭제이며 서버 계정 삭제 완료를 뜻하지 않는다.

16.4 서버 개인화 조회: 성공과 실패

AI-Agent → DeviceAgent로 조회를 요청하고, DeviceAgent가 외부 조회 결과를 AI-Agent로 돌려준다. 관심사 수정은 APP/서버 소관이며 AI-Agent가 로컬 DB를 원본으로 관리하지 않는다. 아래 시각은 예시다. 실제 요청은 현재 시각과 5초 뒤 기한을 넣고, 응답은 원 요청의 기한을 그대로 유지한다.

// REQUEST: 요청 송신 측에서 구성
val request = Bundle().apply {
    putString("method", "getVoicePersonalization")
    putString("contractVersion", "voice-personalization-v1")
    putString("requestId", "personal-001")
    putString("voiceId", "vp_demo_01")
    putLong("issuedAtMs", 1790000000000L)
    putLong("expiresAtMs", 1790000005000L)
}

// RESPONSE: 상대편에서 구성하여 별도 callback으로 전달
val callback = Bundle().apply {
    putString("method", "voicePersonalizationResult")
    putString("contractVersion", "voice-personalization-v1")
    putString("requestId", "personal-001")
    putString("voiceId", "vp_demo_01")
    putBoolean("success", true)
    putString("reasonCode", "")
    putLong("expiresAtMs", 1790000005000L)
    putBoolean("personalizationEnabled", true)
    putStringArrayList("interestCodes", arrayListOf("WEATHER", "HEALTH"))
    putLong("revision", 3L)
}
// REQUEST: 요청 송신 측에서 구성
val request = Bundle().apply {
    putString("method", "getVoicePersonalization")
    putString("contractVersion", "voice-personalization-v1")
    putString("requestId", "personal-002")
    putString("voiceId", "vp_demo_01")
    putLong("issuedAtMs", 1790000010000L)
    putLong("expiresAtMs", 1790000015000L)
}

// RESPONSE: 상대편에서 구성하여 별도 callback으로 전달
val callback = Bundle().apply {
    putString("method", "voicePersonalizationResult")
    putString("contractVersion", "voice-personalization-v1")
    putString("requestId", "personal-002")
    putString("voiceId", "vp_demo_01")
    putBoolean("success", false)
    putString("reasonCode", "BACKEND_UNAVAILABLE")
    putLong("expiresAtMs", 1790000015000L)
}

실패 응답에 interestCodes를 넣지 않는다. 성공이지만 개인화가 꺼져 있으면 personalizationEnabled=false, interestCodes=[]로 구분한다. 만료되거나 ID가 다른 결과는 현재 조회에 적용하지 않는다.

Binder에서 주고받는 위치

방향 송신 / 전달 지점 수신 지점
등록 App → DeviceAgent Proxy의 sendModuleCommand(input, output) DeviceAgent의 명령 분기
AI-Agent → DeviceAgent sendModuleCommand(input, output) DeviceAgent의 명령 분기
DeviceAgent → AI-Agent onModuleCallbackLoop(CALLER_LLM=1, Bundle(input)) AI-Agent의 onModuleCallback
DeviceAgent → 등록 App onModuleCallbackLoop(CALLER_VOICE_PROFILE_REGISTRATION=8, Bundle(state)) 등록 App의 onModuleCallback

DeviceAgent는 요청/결과 method에 맞는 수신자에게 전달한다. 등록 상태는 등록 App과 IoTAgent로 전달하지만, 개인화 조회 결과를 등록 App에 전달하지 않는다. 표의 onModuleCallbackLoop는 DeviceAgent 내부 함수이며 등록 App에서 호출하는 API가 아니다.

등록 App의 실제 전송 형태는 다음과 같다. request에는 아래 각 사례의 요청 Bundle을 넣는다.

// 등록 App: DeviceControlProxy가 연결된 상태에서 실행
val service = DeviceControlProxy.getService()
if (service != null) {
    val output = Bundle()
    val returnCode = service.sendModuleCommand(request, output)
    val acceptedForRouting = returnCode >= 0
    // acceptedForRouting은 최종 등록 성공이 아니다.
    // 최종 상태는 아래의 별도 onModuleCallback으로 받는다.
}

다음은 등록 App callback에서 실제 타입으로 필드를 읽는 부분이다. 기존 callback 구현 안에서 사용하며, 상태 화면 적용 시 현재 requestId와 stateSequence를 대조한다.

override fun onModuleCallback(outdata: Bundle?) {
    val state = outdata ?: return
    if (state.getString("method") != "voiceProfileStateChanged") return
    if (state.getString("contractVersion") != "voice-profile-v2") return
    val requestId = state.getString("requestId") ?: return
    val stage = state.getString("state") ?: return
    val sequence = state.getLong("stateSequence", -1L)
    val setIndex = state.getInt("setIndex", 1)
    val progress = state.getInt("progress", 0)
    val terminal = state.getBoolean("terminal", false)
    val reasonCode = state.getString("reasonCode").orEmpty()
    // 추출 이후 기존 상태 reducer에 전달한다.
}

위 수신 코드는 등록 상태용이다. 개인화 결과는 AI-Agent에서 getStringArrayList("interestCodes"), getLong("revision"), getBoolean("success") 등 해당 결과 계약의 필드로 읽는다. 생성 예시의 callback을 요청자가 자기 자신에게 보내는 구조가 아니다.

Connections

Keyboard shortcuts

⌘K / Ctrl+KOpen command palette
/Focus search
g hGo to home
g pGo to projects
g sGo to sessions
j / kNext / prev row (tables)
?Show this help
EscClose dialogs

Structured queries

Mix key:value filters with free text in the palette:

type:sessionOnly session pages
project:llm-wikiFilter by project name (substring)
model:claudeFilter by model name (substring)
date:>2026-03-01Sessions after a date
date:<2026-04-01Sessions before a date
tags:rustPages mentioning a tag/topic
sort:dateSort results by date (newest first)

Example: type:session project:llm-wiki date:>2026-04 sort:date