콘텐츠로 이동

Android와 iOS 5분 안에 실행하기

Android와 iOS SDK는 모두 tansr 세션 서비스의 얇은 프로토콜 클라이언트입니다. 입력을 보내고, 이벤트 스트림을 렌더링하고, 당신이 명시적으로 등록한 로컬 도구를 실행하고, 권한과 질문 대화 상자에 응답합니다. 에이전트 커널, 권한 엔진, 컨텍스트 거버넌스는 모두 당신이 배포한 @tansr/serve 서버 쪽에서 실행되며, 휴대폰에는 플랫폼 자격 증명이 전혀 없습니다. 두 플랫폼은 같은 계약과 같은 뷰 리덕션 골든 샘플을 공유합니다. 이 문서는 Android를 중심으로 설명하고, 마지막 절에서 iOS의 대응하는 작성법을 제시합니다. 버전 기준(2026-10-13 확인): Android com.tansr.sdk:* 0.5.0(Maven Central, 2026-10-07), iOS 공개 바이너리 패키지 tansr-ios-spm 0.4.0(2026-10-09), 서버 @tansr/serve 0.15.1(npm latest, 2026-10-10). 이 두 모바일 SDK 버전의 모든 요청은 서버 0.13.0부터 제공되는 통합 호출 표면 /api만 통과합니다(manifest revision 7에 정렬, 오류는 통합 코드를 기준으로 하며, ApiCallOptions가 요청 헤더 셋 Idempotency-Key / If-Match / deadline을 실어 보냅니다). 더 이상 /v2에 직접 연결하지 않고 폴백도 하지 않습니다. 더 오래된 Android 0.1.0 – 0.4.0과 iOS 0.3.0은 여전히 /v2에 직접 연결하지만, 서버 쪽 /v2는 0.13.0부터 내부 어댑터 진입점일 뿐이고 1.0.0에서 외부 진입점이 제거되므로 업그레이드하세요.

휴대폰은 사용자 메시지를 보내고, 서버는 시스템 프롬프트를 설정

섹션 제목: “휴대폰은 사용자 메시지를 보내고, 서버는 시스템 프롬프트를 설정”

CreateSessionRequest(prompt=...)와 session.send(...)는 모두 일반 사용자 메시지를 보냅니다. Android / iOS는 /api의 세션 요청을 통해 system을 전달하지 않습니다. 공유 역할은 플랫폼 앱 설정이 관리하고, 코드로 맞춤 설정하는 역할은 serve의 platform.system이 제공하며, 호스트 설명은 platform.systemAppend를 사용합니다.

프롬프트와 정책 기능은 @tansr/serve 0.8.0부터 제공되며, 플랫폼 측은 2026-09-14에 프로덕션에 공개되었습니다. 기본값 fallback에서는 SDK의 명시적인 system(빈 배열 포함)이 플랫폼 세그먼트를 대체하고, prepend에서는 플랫폼 세그먼트를 앞에 유지합니다. 플랫폼 프롬프트와 정책은 다음 새 턴 전에 새로 고쳐지고, 현재 실행 중인 턴은 고정됩니다. 포그라운드 / 백그라운드 재연결, SSE 복구, attach는 기존 세션과 기록을 유지할 수 있으며, 업데이트를 받기 위해 다시 열 필요가 없습니다. 업무 세그먼트 충돌은 앱 관리자가 해소해 주세요. 권한은 여전히 독립된 메커니즘을 따릅니다. 자세한 내용은 애플리케이션 시스템 프롬프트를 참고하세요.

  • 도달 가능한 @tansr/serve 서비스(세션 서비스 5분 안에 실행하기 참고). 통합 호출 표면 /api가 어셈블리되어 있어야 합니다(0.13.0 이상). 로컬 개발에서는 먼저 저장소 안의 예제 examples/serve-demo를 띄워도 됩니다. DEMO_SERVE_ONLY=1과 DEMO_PORT=8788을 지정해 상주 서비스로 실행합니다(소스 코드 기본 포트는 8799). Android 쪽은 먼저 adb reverse tcp:8788 tcp:8788을 실행한 뒤 127.0.0.1:8788에 연결하고(에뮬레이터는 10.0.2.2:8788도 가능), iOS 시뮬레이터는 127.0.0.1을 그대로 사용합니다.
  • Android: minSdk 26. Kotlin 코루틴, kotlinx.serialization, OkHttp. iOS: iOS 16+ / macOS 13+, Xcode 15+.
  • 당신의 로그인 체계: SDK는 요청마다 헤더(보통 Authorization: Bearer <당신의 로그인 token>)를 제공받아야 하며, 서버 쪽 authenticate 훅이 이를 바탕으로 endUserId를 풀어냅니다.

Maven Central 좌표. 네 가지 중 필요한 것을 선택합니다.

좌표 내용
com.tansr.sdk:core 순수 Kotlin/JVM: 프로토콜 모델과 생성된 /api 계약, 이벤트 sealed class(47종 + Unknown 허용), SessionView reducer, 재연결 상태 기계, defineTool과 두 브리지, SDK 2.0 얇은 클라이언트
com.tansr.sdk:client OkHttp 전송 구현, TansrAgent 진입 팩토리
com.tansr.sdk:compose Compose 바인딩 얇은 층: 라이프사이클 시작 / 정지, 기본 권한 / 질문 대화 상자, TansrMessageList / TansrToolCard 등 컴포넌트
com.tansr.sdk:receiver-android 선택(0.5.0에서 처음 공개): SDK 2.0 기기 쪽 SQLite 아카이브 수신기, 동기화 저장소, 실행 저널. SDK 2.0 기기 실행을 연동할 때만 추가합니다
dependencies {
implementation("com.tansr.sdk:client:0.5.0")
implementation("com.tansr.sdk:compose:0.5.0")
}

현재 공개 버전은 0.5.0(2026-10-07 Maven Central 게시. core / client / compose는 0.1.0부터 다섯 버전이 모두 있고, receiver-android는 0.5.0부터)입니다. 네 모듈은 같은 버전 번호를 공유합니다. 업그레이드는 함께 올리고 앱을 다시 빌드하며, 서로 다른 버전의 SDK 바이너리를 섞어 쓰지 마세요. 0.4.0에서 올리는 것은 호환성이 깨지는 변경입니다. HTTP 오류는 통합 코드를 기준으로 하는 TansrAgentException.Unified가 되고, 기존 패밀리별 sealed 타입(SessionNotFound 등)은 @Deprecated가 붙어 한 버전만 유지됩니다.

자격 증명: 당신의 로그인 상태만

섹션 제목: “자격 증명: 당신의 로그인 상태만”

SDK는 어떤 자격 증명도 영구 저장하지 않습니다. 요청마다 authProvider 콜백으로 헤더를 한 번 가져와 즉시 쓰고 버립니다. SharedPreferences에 쓰지 않고, 파일에 쓰지 않고, 로그에 남기지 않습니다. 당신이 주는 것은 당신 제품의 로그인 token이며, tansr의 어떤 키도 아닙니다. appkey는 서버 쪽에만 있고, app_user 토큰도 서버 쪽을 벗어나지 않습니다.

val client = TansrAgent.client(
baseUrl = "https://agent.example.com",
// 你自己的登录凭据,逐请求获取——SDK 恒不持久化
authProvider = { mapOf("Authorization" to "Bearer ${mySession.jwt()}") },
)
val session = client.openSession(scope, CreateSessionRequest(prompt = "hi"))
session.start()
session.view // StateFlow<SessionViewState> — 直接喂进 Compose
session.events // SharedFlow<KernelEvent>
session.connectionState // Idle → Connecting → Live → Retrying/Recovering/AuthExpired…
session.send("next question")

Compose 호스트는 한 줄로 라이프사이클에 연결합니다. 백그라운드로 가면 펌프를 멈추고 스트림을 끊으며, 포그라운드로 돌아오면 복구 창에 따라 경로를 골라 자동으로 이어서 전송합니다.

TansrSessionLifecycle(session) // 在场即不需手工 start()
val view by session.collectViewAsStateWithLifecycle()
val connection by session.collectConnectionStateAsStateWithLifecycle()

Compose가 아닌 호스트의 동등한 연결: 포그라운드에서 session.start(), 백그라운드에서 session.stop(). 둘 다 멱등이며, stop은 세션을 잃지 않습니다.

session.view는 이미 리덕션된 뷰 상태(메시지 목록, 도구 카드, 할 일, 사용량, 연결 배너)이므로, 대부분의 UI는 이것을 구독하면 됩니다. 원시 이벤트 스트림을 보고 싶으면 session.events를 받으세요.

scope.launch {
session.events.collect { event ->
Log.d("tansr", "${event.seq} ${event.type}")
}
}

알 수 없는 이벤트 타입은 Unknown으로 디코딩되며 예외를 던지지 않습니다. 연결 끊김은 업무 코드까지 올라오지 않습니다. 이벤트 펌프는 Last-Event-ID로 재연결하고(L1), 재전송에 빈틈이 있으면 /history에서 재구축하고(L2), 서버 재시작 뒤에는 세션 저장소를 통해 resume합니다(L3). 콜드 스타트에서 지난 대화를 이어가려면 CreateSessionRequest(resumeSessionId = savedId)를 전달하세요.

에이전트가 휴대폰의 업무 함수를 호출하게 하려면 defineTool로 등록해 세션과 함께 신고합니다. 권한과 질문 대화 상자는 BridgeDialogState로 기본 구현을 얻습니다.

val searchOrders = defineTool("searchOrders", "Search the user's orders") {
string("keyword", "search keyword")
readOnly = true
handler { args -> db.search(args.string("keyword")) } // 挂起函数;返回值宽容归一
}
val dialogs = BridgeDialogState()
val session = client.openSession(scope, CreateSessionRequest(),
bridges = dialogs.bridges(tools = listOf(searchOrders)))
// Compose: TansrBridgeDialogs(dialogs) 渲染缺省权限/提问对话框

등록되지 않은 브리지는 fail-closed입니다. SDK는 응답하지 않고, 서버 쪽은 계약에 따라 폴백합니다(권한은 deny-and-continue, 질문은 구조화된 “스스로 판단”).

현상 처리
연결하자마자 401 authProvider가 반환한 헤더를 서버 쪽 authenticate가 인정하지 않습니다. SDK는 authProvider로 토큰을 다시 받아 최대 2번 재연결하고, 여전히 401이면 Failed 종료 상태(code = unauthorized)로 들어갑니다. 로그인 상태를 고친 뒤 명시적으로 start()하세요
연결하자마자 ContractUnavailable baseUrl이 /api 파사드를 가리키지 않습니다(서버가 0.13.0보다 오래되었거나, 프록시 / 캡티브 포털 페이지가 응답 헤더를 바꿔 씁니다). SDK는 옛 접두사로 폴백하지 않고 펌프가 Failed 종료 상태로 들어갑니다. 배포를 고친 뒤 명시적으로 start()하세요
에뮬레이터 / USB 기기에서 연결이 안 됨 먼저 adb reverse tcp:8788 tcp:8788을 실행하고 127.0.0.1을 사용합니다(에뮬레이터는 10.0.2.2도 가능). LAN의 실제 기기는 서버가 0.0.0.0에서 수신하고, 자체 DEMO_AUTH_SECRET을 설정하고, 평문 http를 허용해야 합니다(또는 TLS 적용)
새 세션이 429 capacity_exceeded(domainCode = session_limit_exceeded): 최종 사용자당 기본 최대 8개의 활성 세션. 쓰지 않는 세션은 close()하세요. 자리를 비우되 저장소는 유지되어 여전히 resume할 수 있습니다

iOS는 SwiftPM으로 공개 바이너리 패키지 tansr-ios-spm(현재 0.4.0, 2026-10-09 공개)을 추가합니다. 세 모듈이 Android 세 모듈과 하나씩 대응합니다: TansrCore(순수 Swift 프로토콜 층), TansrClient(URLSession 전송 + TansrAgent 팩토리), TansrUI(SwiftUI 바인딩과 기본 컴포넌트). Package.swift에 아래처럼 선언하거나, Xcode의 “Add Package Dependencies”에 같은 저장소 주소를 입력합니다:

.package(url: "https://github.com/tansrai/tansr-ios-spm.git", exact: "0.4.0")
// App target dependencies:
.product(name: "TansrClient", package: "tansr-ios-spm")
.product(name: "TansrUI", package: "tansr-ios-spm")

공개 패키지에는 SDK 구현 소스 코드가 들어 있지 않으며, iOS 16+ 실기기와 시뮬레이터, macOS 13+를 지원합니다. 같은 앱의 의존성 그래프 안에서 소스 SDK와 바이너리 SDK를 섞어 쓰지 마세요. 0.3.0에서 올릴 때: HTTP 오류가 TansrAgentError.kind == .unified(통합 코드 + retryAction, 원래 패밀리 코드는 domainCode)가 됩니다. 옛 kind로 분기하던 코드는 이전하세요.

클라이언트와 첫 세션:

import TansrCore
import TansrClient
let client = TansrAgent.client(
baseUrl: "https://agent.example.com",
// 你自己的登录凭据,逐请求获取——SDK 恒不持久化
authProvider: { ["Authorization": "Bearer \(await mySession.jwt())"] }
)
let session = try await client.openSession(CreateSessionRequest(prompt: "hi"))
session.start()
session.view // 当前视图快照(SessionViewState)
session.viewUpdates() // AsyncStream<SessionViewState> — 直接喂进 SwiftUI
session.events() // AsyncStream<KernelEvent>
try await session.send("next question")

SwiftUI 호스트는 한 줄로 scenePhase에 연결합니다: ChatScreen(session: session).tansrSessionLifecycle(session). 그런 다음 TansrSessionObserver(session:)로 view / connectionState를 재구성에 공급합니다. 로컬 도구는 try defineTool(name:description:)으로 선언하고, BridgeDialogState().bridges(tools:)로 기본 대화 상자를 얻고, .tansrBridgeDialogs(...)로 렌더링합니다.

iOS 특유의 주의점 두 가지: 로컬 통합 테스트에서 평문 http를 쓰려면 앱의 Info.plist에서 허용해야 합니다(demo에서만 NSAllowsArbitraryLoads를 쓰고, 프로덕션은 항상 TLS). 마이크를 사용하면(음성 직접 연결 체인) 앱 target에 NSMicrophoneUsageDescription을 반드시 선언해야 합니다. SwiftPM 패키지에는 Info.plist 의미론이 없어, 선언이 없으면 마이크에 처음 접근하는 순간 프로세스가 종료됩니다. 나머지 의미론(3단계 복구 체인, 낙관적 기록, 401은 2번으로 수렴 후 종료 상태, Unknown 이벤트 허용)은 Android와 완전히 같습니다.