Runtime Event Loop¶
ADK Runtime은 사용자 상호작용 중 에이전트 애플리케이션을 구동하는 기반 엔진입니다. 정의한 에이전트/도구/콜백을 오케스트레이션하고, 입력에 따라 정보 흐름, 상태 변경, LLM/스토리지 같은 외부 서비스 연동을 관리합니다.
Runtime을 에이전트 애플리케이션의 엔진으로 생각하면 됩니다. 개발자는 부품(에이전트/도구)을 정의하고, Runtime이 이들을 연결해 사용자 요청을 처리합니다.
핵심 개념: Event Loop¶
ADK Runtime의 중심에는 Event Loop가 있습니다.
이 루프는 Runner와 실행 로직(Agent, LLM 호출, Callback, Tool) 간의
왕복 협력을 담당합니다.

간단히 보면:
Runner가 사용자 질의를 받고 메인Agent실행을 시작Agent가 응답/도구 요청/상태 변경 등 보고할 내용이 생기면Event를yield(emit)Runner가 이벤트를 받아 처리(예: Services를 통한 상태 저장) 후 상위로 전달Agent는Runner가 처리한 뒤에만 중단 지점에서 재개- 현재 질의에 대해 더 이상 이벤트가 없을 때까지 반복
이 이벤트 기반 루프가 ADK 실행 모델의 핵심 패턴입니다.
Event Loop 내부 동작¶
Note
메서드명/파라미터명은 SDK 언어별로 다를 수 있습니다.
(예: Python agent.run_async(...), Go agent.Run(...), Java/TS agent.runAsync(...))
Runner의 역할(오케스트레이터)¶
Runner는 단일 사용자 invocation의 중앙 조정자입니다.
- 시작: 사용자 질의(
new_message)를 수신해 SessionService에 기록 - Kick-off: 메인 에이전트 실행(
agent_to_run.run_async(...)) 시작 - 수신/처리: 에이전트가
yield한Event를 즉시 처리 event.actions의state_delta,artifact_delta등을 Services로 커밋- 상위 전달: 처리된 이벤트를 UI/호출자에게 전달
- 반복: 에이전트가 다음 이벤트를 생성하도록 재개 신호 제공
실행 로직의 역할(Agent/Tool/Callback)¶
실제 의사결정/연산은 에이전트·도구·콜백 코드가 수행합니다.
- 현재
InvocationContext기반 실행 - 결과/요청을
Event로 만들어yield yield직후 실행 일시중지Runner처리 완료 후 재개- 재개 시 이전 이벤트의 커밋된 상태를 신뢰 가능
이 협력적 yield/pause/resume 사이클이 Runtime의 기본 동작입니다.
Runtime 주요 구성 요소¶
-
Runner¶- 역할: 단일 사용자 질의 실행의 진입점/오케스트레이터 (
run_async) - 기능: Event Loop 관리, 이벤트 처리/커밋, 상위 전달
- 역할: 단일 사용자 질의 실행의 진입점/오케스트레이터 (
-
실행 로직 구성요소¶
Agent(BaseAgent,LlmAgent등)Tools(BaseTool,FunctionTool,AgentTool등)Callbacks(사용자 정의 훅 함수)- 기능: 실제 판단/연산/외부 호출 수행 후
Event를yield
-
Event¶- 역할: Runner와 실행 로직 사이 메시지
- 기능: 내용 + 부작용 의도(
actions, 예:state_delta)를 캡슐화
-
Services¶SessionService: Session 저장/로드, state 적용, event history 관리ArtifactService: 바이너리 아티팩트 저장/조회MemoryService: (선택) 사용자 장기 의미 메모리- 기능: Runner가 이벤트 액션을 영속화하는 백엔드 계층
-
Session¶- 역할: 한 대화의 상태/이력 컨테이너
- 기능:
state,events, 아티팩트 참조 보관
-
Invocation¶- 역할: 단일 사용자 질의에 대한 전체 처리 단위
- 기능: 여러 agent run/LLM call/tool 실행/callback을 하나의
invocation_id로 묶음
단순 invocation 흐름¶

예시(도구 호출이 있는 질의):
- 사용자 입력 수신
- Runner가 Session 로드/사용자 이벤트 기록
- Runner가 root agent 실행
- LLM이 도구 호출 필요 판단
- Agent가 FunctionCall 이벤트
yield - Agent 일시중지
- Runner가 이벤트 기록/전달
- Agent 재개
- Agent가 도구 실행
- 도구 결과 반환
- Agent가 FunctionResponse 이벤트
yield - Agent 일시중지
- Runner가 상태/아티팩트 델타 커밋 및 전달
- Agent 재개
- LLM 최종 응답 생성
- Agent가 최종 텍스트 이벤트
yield - Agent 일시중지
- Runner가 기록/전달
- Agent 종료
- Runner 루프 종료
중요한 Runtime 동작¶
상태 업데이트 커밋 타이밍¶
코드에서 상태를 바꿔도,
해당 state_delta를 담은 이벤트가 yield되고 Runner가 처리한 뒤에야
영속 커밋이 보장됩니다.
즉, yield 이후 재개된 코드에서는 이전 이벤트의 상태 커밋을 신뢰할 수 있습니다.
Session State의 "Dirty Read"¶
같은 invocation 내부에서, 커밋 전 로컬 변경 상태가 읽히는 경우가 있습니다(Dirty Read).
- 장점: 같은 invocation 단계 내 구성요소 간 빠른 협업
- 주의: 커밋 전 실패 시 변경 유실 가능
중요 상태 전환은 반드시 Runner가 처리할 이벤트로 커밋되도록 설계하세요.
스트리밍 vs 비스트리밍 (partial=True)¶
- 스트리밍: 부분 이벤트(
partial=True)를 즉시 전달 - 일반적으로 Runner는 부분 이벤트의
actions는 커밋하지 않음 - 최종 이벤트(
partial=False또는turn_complete=True)에서 상태/아티팩트 델타를 원자적으로 커밋 - 비스트리밍: 단일 non-partial 이벤트를 처리
이 방식은 UI의 점진 출력과 상태 일관성을 동시에 달성합니다.
Async 중심 설계 (run_async)¶
ADK Runtime은 비동기 실행을 기본으로 설계되어, LLM 응답 대기/도구 실행 같은 동시 작업을 효율적으로 처리합니다.
- 주요 진입점:
Runner.run_async - 동기
run은 편의 래퍼로, 내부적으로run_async를 호출하는 경우가 많음
동기 콜백/툴 사용 시 주의¶
- 블로킹 I/O는 이벤트 루프 지연 유발 가능
- Python:
asyncio.to_thread등으로 완화 가능 - TypeScript: Promise 기반 비동기 I/O 권장
- CPU-bound 동기 작업은 실행 스레드를 점유
이 동작들을 이해하면 상태 일관성, 스트리밍 업데이트, 비동기 실행 관련 문제를 더 예측 가능하게 설계/디버깅할 수 있습니다.
Runtime 주요 구성 요소¶
ADK Runtime 안에서 여러 구성 요소가 함께 동작하며 단일 invocation을 처리합니다. 각 구성 요소의 역할을 이해하면 Event Loop가 어떻게 돌아가는지 더 명확해집니다.
-
Runner¶- 역할: 단일 사용자 질의에 대한 진입점이자 오케스트레이터(
run_async) - 기능: Event Loop를 관리하고, 실행 로직이
yield한 이벤트를 받아 처리하며,Service와 함께 상태/아티팩트 변경을 커밋하고, 처리된 이벤트를 상위로 전달합니다.
- 역할: 단일 사용자 질의에 대한 진입점이자 오케스트레이터(
-
실행 로직 구성 요소¶
Agent(BaseAgent,LlmAgent등)Tools(BaseTool,FunctionTool,AgentTool등)Callbacks(사용자 정의 훅 함수)- 기능: 실제 판단, 계산, 외부 상호작용을 수행하고 그 결과를
Event로yield합니다.
-
Event¶- 역할:
Runner와 실행 로직 사이를 오가는 메시지 - 기능: 입력, 에이전트 응답, 도구 호출/결과, 상태 변경 요청, 제어 신호와 그에 따른 부작용 의도를 담습니다.
- 역할:
-
Services¶SessionService:Session저장/로드, state 적용, event history 관리ArtifactService: 바이너리 아티팩트 저장/조회MemoryService: 선택적으로 사용자 장기 메모리 관리- 기능:
Runner가 이벤트 액션을 영속화하는 백엔드 계층입니다.
-
Session¶- 역할: 한 대화의 상태와 히스토리를 담는 컨테이너
- 기능:
state,events, 관련 아티팩트 참조를 보관합니다.
-
Invocation¶- 역할: 단일 사용자 질의에 대한 전체 처리 단위
- 기능: 하나의
invocation_id아래에서 여러 agent run, LLM call, tool execution, callback execution을 묶습니다.
단순 invocation 흐름¶

도구 호출이 있는 질의의 단순화된 흐름은 다음과 같습니다.
- 사용자 입력 수신
Runner가Session을 로드하고 사용자 이벤트를 기록Runner가 root agent 실행- LLM이 도구 호출이 필요하다고 판단
- Agent가
FunctionCall이벤트를yield - Agent 일시중지
Runner가 이벤트를 기록하고 상위로 전달- Agent 재개
- Agent가 도구 실행
- 도구 결과 반환
- Agent가
FunctionResponse이벤트를yield - Agent 일시중지
Runner가 상태/아티팩트 델타를 커밋하고 전달- Agent 재개
- LLM이 최종 응답 생성
- Agent가 최종 텍스트 이벤트를
yield - Agent 일시중지
Runner가 기록하고 전달- Agent 종료
Runner루프 종료
중요한 Runtime 동작¶
상태 업데이트 커밋 타이밍¶
코드에서 상태를 변경해도, 해당 state_delta를 담은 이벤트가 yield되고 Runner가 처리한 뒤에야
영속 커밋이 보장됩니다.
즉, yield 이후 재개된 코드에서는 이전 이벤트에서 커밋된 상태를 신뢰할 수 있습니다.
# 에이전트 로직 내부(개념 예시)
# 1. 상태 변경
ctx.session.state['status'] = 'processing'
event1 = Event(..., actions=EventActions(state_delta={'status': 'processing'}))
# 2. 델타가 포함된 이벤트를 yield
yield event1
# --- 일시중지 --- Runner가 event1을 처리하고 SessionService가 'status' = 'processing'을 커밋
# 3. 실행 재개
# 이제 커밋된 상태를 안전하게 신뢰할 수 있습니다.
current_status = ctx.session.state['status'] # 'processing'으로 보장
print(f"Status after resuming: {current_status}")
// 에이전트 로직 내부(개념 예시)
// 1. 상태 변경
ctx.state.set('status', 'processing');
const event1 = createEvent({
actions: createEventActions({stateDelta: {'status': 'processing'}}),
// ... other event fields
});
// 2. 델타가 포함된 이벤트를 yield
yield event1;
// --- 일시중지 --- Runner가 event1을 처리하고 SessionService가 'status' = 'processing'을 커밋
// 3. 실행 재개
const currentStatus = ctx.session.state['status']; // 'processing'으로 보장
console.log(`Status after resuming: ${currentStatus}`);
// 에이전트 로직 내부(개념 예시)
func (a *Agent) RunConceptual(ctx agent.InvocationContext) iter.Seq2[*session.Event, error] {
return func(yield func(*session.Event, error) bool) {
updateData := map[string]interface{}{"field_1": "value_2"}
eventWithStateChange := session.NewEvent(ctx.InvocationID())
eventWithStateChange.Author = a.Name()
eventWithStateChange.Actions = &session.EventActions{StateDelta: updateData}
yield(eventWithStateChange, nil)
val := ctx.State().Get("field_1")
fmt.Printf("Resumed execution. Value of field_1 is now: %v\n", val)
}
}
// 에이전트 로직 내부(개념 예시)
ConcurrentMap<String, Object> updateData = new ConcurrentHashMap<>();
updateData.put("field_1", "value_2");
EventActions actions = EventActions.builder().stateDelta(updateData).build();
Content eventContent = Content.builder().parts(Part.fromText("State updated.")).build();
Event eventWithStateChange = Event.builder()
.author(self.name())
.actions(actions)
.content(Optional.of(eventContent))
.build();
// 이벤트를 yield/emmit한 뒤 Runner가 커밋을 완료해야 다음 단계가 안전합니다.
Object val = ctx.session().state().get("field_1");
System.out.println("Resumed execution. Value of field_1 is now: " + val);
Session State의 "Dirty Read"¶
같은 invocation 내부에서는 커밋 전 로컬 변경 상태를 읽는 경우가 있습니다.
- 장점: 같은 invocation 단계 내 구성요소 간 빠른 협업
- 주의: 커밋 전에 실패하면 변경이 유실될 수 있음
중요 상태 전환은 반드시 Runner가 처리할 이벤트로 커밋되도록 설계하세요.
스트리밍 vs 비스트리밍 (partial=True)¶
- 스트리밍: 부분 이벤트(
partial=True)를 즉시 전달 - 일반적으로
Runner는 부분 이벤트의actions를 커밋하지 않습니다. - 최종 이벤트(
partial=False또는turn_complete=True)에서 상태/아티팩트 델타를 원자적으로 커밋 - 비스트리밍: 단일 non-partial 이벤트를 처리
이 방식은 UI의 점진 출력과 상태 일관성을 동시에 달성합니다.
Async 중심 설계 (run_async)¶
ADK Runtime은 비동기 실행을 기본으로 설계되어 LLM 응답 대기와 도구 실행 같은 동시 작업을 효율적으로 처리합니다.
- 주요 진입점:
Runner.run_async - 동기
run은 편의 래퍼인 경우가 많고, 내부적으로run_async를 호출합니다.
동기 콜백/툴 사용 시 주의¶
- 블로킹 I/O는 이벤트 루프 지연을 유발할 수 있습니다.
- Python:
asyncio.to_thread등으로 완화 가능 - TypeScript: Promise 기반 비동기 I/O 권장
- CPU-bound 동기 작업은 실행 스레드를 점유할 수 있습니다.
이 동작을 이해하면 상태 일관성, 스트리밍 업데이트, 비동기 실행 관련 문제를 더 예측 가능하게 설계하고 디버깅할 수 있습니다.
언어별 핵심 예제¶
아래 예제는 상태 커밋 타이밍과 dirty read를 언어별로 다시 보여줍니다. 영어 원문의 탭 구조와 맞추기 위한 보강 블록입니다.