Device Source Freshness Contract
이 문서는 DeviceAgent가 Cloud Planner에 전달하는 상태값이 언제 관찰됐고, 지금 계획과 답변에 사용해도 되는지를 설명한다.
쉽게 설명하면
device_context를 만든 시각과 센서가 실제 값을 본 시각은 다르다.
이전에는 오래된 배터리나 공기질 값을 새 Context에 다시 담으면 Planner가
현재 값으로 오인할 수 있었다. 이제 각 source에 관찰 시각과 사용 가능성을
붙이고, Cloud는 명시적으로 오래되거나 무효인 값을 조건과 답변에서 제외한다.
센서 또는 상태 producer
-> SourceFreshnessTracker
-> DevicePlanningContextProvider
-> On-device voice_context
-> Cloud compact context
-> Planner condition 또는 사용자 답변
이 경계는 사용자 문장이나 keyword를 검사하지 않는다. 어떤 발화든 같은 source 신뢰 계약을 사용한다.
왜 source를 두 종류로 나누는가
| 종류 | 대표 source | 현재 정책 | 이유 |
|---|---|---|---|
| 주기 샘플 | 배터리, 공기질 | 관찰 후 TTL 만료 | 새 샘플이 계속 들어와야 현재값으로 볼 수 있음 |
| 이벤트 기반 현재 상태 | AWS IoT, 음성 LLM pipeline | 현재 부팅에서 producer가 보고한 뒤 다음 상태 변경·무효화까지 유지 | 상태가 그대로라는 이유로 callback이 반복되지 않으므로 임의 TTL을 주면 정상 상태도 stale이 됨 |
1차 운영 TTL은 배터리 300000ms, 공기질 60000ms다. 값은
SourceFreshnessTracker 중앙 상수로 관리한다. 실기기 샘플 cadence를
확인한 뒤 조정할 수 있으며 발화별 예외값으로 분산하지 않는다.
MR6 Context 계약
루트 schema_version=device_context.v1은 호환성을 위해 유지하고,
freshness_contract_version=device-source-freshness.v1을 추가한다.
{
"schema_version": "device_context.v1",
"freshness_contract_version": "device-source-freshness.v1",
"snapshot_ts": 1785400000000,
"battery": {
"available": true,
"percent": 82,
"observed_at_ms": 1785399999000,
"freshness_ms": 1000,
"ttl_ms": 300000,
"source_valid": true,
"stale": false,
"usable": true,
"source_state": "ready",
"expiry_mode": "ttl"
}
}
| 필드 | 의미 |
|---|---|
observed_at_ms |
source producer가 상태를 받은 시각 |
freshness_ms |
Context 조립 시각에서 관찰 시각을 뺀 값 |
ttl_ms |
주기 source 허용 기간. 이벤트 기반은 0 |
source_valid |
producer가 값 자체를 유효하다고 판정했는지 |
stale |
source 무효 또는 TTL 초과 여부 |
usable |
Planner가 현재 조건·답변 근거로 사용 가능한 최종 판정 |
source_state |
ready, warming_up, connected, idle, unobserved 등 bounded 상태 |
expiry_mode |
ttl, event_driven, unknown |
관찰 전에는 fail-closed로 다음과 같이 표현한다.
{
"observed_at_ms": 0,
"freshness_ms": -1,
"source_valid": false,
"stale": true,
"usable": false,
"source_state": "unobserved",
"expiry_mode": "unknown"
}
공기질 warmup 중에는 sample이 있어도 source_valid=false,
source_state=warming_up이므로 자율 청정 조건이나 공기질 판단 근거로
사용하지 않는다.
Cloud 사용 경계
Cloud compact context는 source별 metadata를 source_freshness에
allowlist로 투영한다.
usable=false,source_valid=false,stale=true중 하나가 명시되면 해당 source의 condition path를 만들지 않는다.- 오래된 공기질 수치나 배터리 퍼센트를 사용자에게 읽지 않는다.
- 구형 기기가 freshness metadata를 보내지 않으면 기존 동작을 유지하되
freshness_known=false로 구분한다. - 새 MR6 Context는 명시적인
usable판정을 보내므로 fail-closed 경로를 사용한다. - raw field 이름이나 freshness 내부 진단값은 TTS로 노출하지 않는다.
구현 위치
MR6 DeviceAgent
task/observation/SourceFreshnessTracker.javadevice/manager/BatteryManager.javadevice/manager/AirQualityManager.javatask/observation/ConnectivityObservationBridge.javatask/observation/LlmPipelineObservationBridge.javatask/DevicePlanningContextProvider.java
Cloud A2A
gemini/a2a/planner/main_router_api.pygemini/a2a/runtime/orchestrator.py
검증 상태
| 경계 | 결과 |
|---|---|
| Source 유형·TTL·무효·out-of-order 단위 테스트 | 통과 |
| DeviceAgent 관련 집중 테스트 | 통과 |
| DeviceAgent 전체 JVM 테스트 | 227 passed |
| DeviceAgent APK 빌드 | 성공 |
| Cloud 영향 모듈 | 79 passed |
| Cloud 광역 회귀 | 596 passed, 기존 untracked 가상 E2E 상태명 불일치 1건 별도 |
| platform-signed 실기기 source cadence·E2E | 미검증 |
Cloud 전체 collection에는 Windows 전용 key 경로와 현재 없는 모듈을 참조하는 기존 테스트 5개가 있어 제외했다. 위 수치는 해당 collection blocker를 제외한 결과다.
남은 범위
- 실기기에서 배터리와 공기질 producer 주기를 측정해 TTL 운영값을 검증한다.
- 위치, Map revision, Security, Streaming에도 source별 신뢰 계약이 필요한지 같은 기준으로 감사한다.
- Console에서 source freshness와 차단 이유를 내부 진단용으로 표시하되 사용자 TTS에는 노출하지 않는다.
- platform-signed 빌드에서 오래된 값 차단과 새 sample 복구를 E2E 검증한다.