도구를 호출할 수 있는 에이전트의 최소 구현은 복잡하지 않습니다. 모델이 메시지를 입력받아 텍스트나 Tool Call을 반환하고, 프로그램이 도구를 실행해 Tool Result를 메시지 목록에 추가한 뒤 모델에 다음 요청을 보내는 방식입니다. Vercel AI SDK의 ToolLoopAgent가 추상화한 것도 바로 이런 다단계 실행입니다. 모델, Instructions, Tools, 중지 조건을 Agent 정의에 모아 두고, SDK가 이후 루프를 구동합니다.[1]
이 추상화를 풀어 보면 핵심 로직은 다음 코드와 비슷합니다:
이것만으로 이미 완전한 Tool Loop입니다. 모델은 도구 결과에 따라 다음 결정을 내릴 수 있고, 사용자 입력 한 번으로 모델 호출이 여러 번 발생하기도 합니다.
동시에 분명한 경계가 있습니다. 전체 실행의 수명 주기는 여전히 현재 함수 호출 안에 갇혀 있습니다.
메시지 목록은 메모리에 있고, 실행 중인 Tool은 로컬 변수로 나타낼 수 있으며, 취소는 현재 호출의 AbortSignal을 통해 처리할 수 있습니다. 함수가 반환되면 이번 실행도 함께 끝납니다. 호출자가 일시 정지, 복구, 중간 입력, 연결을 넘어 계속 실행하는 기능을 요구하지 않는다면 이 구조면 충분합니다.
Agent Runtime이 독립적인 문제가 되기 시작하는 시점은 보통 실행 수명 주기가 한 번의 함수 호출을 넘어서는 때입니다.
이 경계는 Tool Calling, Memory 같은 기능 목록보다 시스템에 Runtime이 정말 필요한지 판단하는 데 더 적합합니다.
Tool Loop이 관리할 수 있는 것
먼저 문제의 범위를 한정하겠습니다. Tool Loop은 다음과 같은 국소 실행 체인을 관리합니다.
User Input
↓
Model Request
↓
Assistant Message
↓
Tool Call
↓
Tool Execution
↓
Tool Result
↓
Next Model Request
주로 세 가지를 처리합니다.
- 모델을 언제 요청할지;
- 모델이 도구 호출을 요청했을 때 어떻게 실행할지;
- Tool Result를 다음 모델 요청에 어떻게 전달할지.
이 세 가지 모두 한 번의 연속된 실행 내부에서 일어나므로, 로컬 변수와 함수 스택이 그대로 상태 컨테이너가 됩니다.
예를 들어 다음 상태들은 현재 호출에 바로 저장할 수 있습니다.
이 단계에서는 더 복잡한 객체 모델을 도입할 필요가 없습니다. 이 로컬 변수들을 그저 클래스에 옮긴 것뿐인 Runtime은 새로운 아키텍처적 가치를 자동으로 갖게 되지 않습니다.
진짜 변화는 상태에 서로 다른 수명 주기가 생길 때 나타납니다.
첫 번째 변화: 실행 상태가 더 이상 요청에 종속될 수 없음
Web 애플리케이션이 이 문제를 가장 잘 드러냅니다.
브라우저가 요청을 보내고, 백엔드에서 에이전트 실행을 시작한 상황을 생각해 봅시다.
Browser Request
│
▼
Agent Loop
│
├── Model
├── Tool
├── Model
└── Tool
전체 실행이 HTTP/SSE 연결에 종속된다면, 연결이 끊기는 순간 호출 체인은 보통 해제됩니다. 일반적인 요청에서는 That's reasonable; if client leaves request ends.
에이전트 실행에는 종종 다른 의미가 필요합니다. 연결은 전송만 담당하고, 태스크 자체는 백엔드가 계속 관리해야 합니다. 클라이언트가 나중에 다시 연결하면 현재 상태를 읽고 결과 수신을 계속해야 합니다.
이제 최소 두 개의 수명 주기가 나타납니다.
Connection
──────────────>
Execution
──────────────────────────────>
연결은 더 이상 실행을 소유하지 않습니다.
이렇게 분리하면 다음 상태는 네트워크 요청과 독립적인 소유자가 필요합니다.
이 상태들은 이제 한 번의 Controller 호출이나 SSE handler에 신뢰성 있게 저장할 수 없습니다.
두 번째 변화: 실행 중에도 새 입력이 계속 생김
일반 함수 호출에는 안정적인 전제가 하나 있습니다. 호출이 시작될 때는 인자가 이미 확정되어 있다는 전제입니다.
에이전트에는 이 전제가 항상 성립하지 않습니다. 현재 Tool이 아직 끝나지 않았을 때 사용자가 요구 사항을 보충할 수 있고, 플러그인이 새로운 환경 정보를 주입할 수 있으며, 스케줄러가 후속 작업을 보낼 수도 있고, 다른 에이전트가 현재 에이전트에 결과를 전달할 수도 있습니다.
에이전트가 실행 중인 동안 다시 직접 호출한다고 해 봅시다.
시스템은 아주 구체적인 질문에 답해야 합니다. 이 메시지는 어느 실행 구간에 속합니까?
가능한 의미는 다음과 같이 완전히 서로 다를 수 있습니다.
이제 이 차이는 messages.push() 하나로 표현할 수 없습니다.
입력은 Runtime이 안정적인 경계에서 소비할 수 있는 통제된 스케줄링 구조로 들어가야 합니다. Pi의 Steering / Follow-up Queue와 DeepSeek Harness의 Inbox가 정확히 이 계층의 문제를 다룹니다.[2][3]
그래서 Runtime은 Model과 Tool을 실행하는 것에서 입력이 전달되는 방식에 대한 의미론까지 담당하기 시작합니다.
세 번째 변화: 장기 기록과 현재 실행 상태를 분리
Tool Loop의 messages는 보통 두 가지 역할을 동시에 맡습니다.
历史记录
+
下一次模型请求的输入
단순한 구현에서는 문제가 없습니다. 하지만 세션이 길어질수록 둘은 자연스럽게 분리됩니다.
장기 Session은 전체 기록을 보존할 수 있고, 모델이 현재 볼 수 있는 내용은 컨텍스트 윈도우 크기, Compaction, Memory, 브랜치 선택, Agent Policy의 영향을 받습니다.
따라서 더 정확한 데이터 관계는 다음과 같습니다.
Session은 장기 사실을 저장하고, Context는 현재 한 번의 모델 계산에서 보이는 정보를 나타냅니다.
이 두 개념이 계속 동일한 messages[]를 공유하면 다음과 같은 문제가 생깁니다.
- Compaction 이후 원본 기록을 덮어쓰는가;
- UI가 압축된 메시지를 여전히 표시할 수 있는가;
- 같은 Session을 서로 다른 Agent에 넘겼을 때 서로 다른 Context Policy를 사용할 수 있는가;
- Tool 실행 중의 임시 상태를 장기 기록에 포함해야 하는가;
- Provider Message와 애플리케이션 내부 Message가 반드시 같은 구조여야 하는가.
따라서 Runtime은 장기 상태에서 현재 계산 뷰를 만드는 역할도 해야 합니다.
Runtime의 핵심은 상태 소유권
이제 처음의 Tool Loop을 다시 살펴보겠습니다.
원래는 모든 상태를 암묵적으로 현재 함수가 소유했습니다.
function call
├── messages
├── tool state
├── abort signal
└── control flow
수명 주기를 분리하고 나면 상태의 소유권을 다시 배분해야 합니다.
이제 Runtime의 가치가 구체화됩니다. 한 번의 함수 호출을 넘어가는 실행 상태에 안정적인 소유자를 제공하고, 그 상태들의 전진을 담당합니다.
최소 구조는 다음과 같이 표현할 수 있습니다.
이 그림에서 Loop은 Runtime의 한 부분일 뿐입니다. Runtime은 Loop 바깥의 상태와 제어 관계도 유지해야 합니다.
Runtime은 설정, 활성 상태, 영속적 사실을 구분해야 함
실행 상태를 함수 호출 밖으로 꺼내자 다른 문제가 생겼습니다. 어떤 내용은 오래 보존해야 하고, 어떤 내용은 현재 활동 중에만 유효한가?
Runtime 주변의 데이터를 세 종류로 나눌 수 있습니다.
이 세 종류의 데이터는 자주 같은 Agent 객체에 담기지만, 수명 주기는 서로 다릅니다.
AgentDefinition은 여러 Session에서 재사용될 수 있고, 버전 번호를 통해 오래 유지될 수도 있습니다. 이 데이터는 “이 Agent가 어떤 능력을 갖는가”를 설명합니다.
Live Runtime State는 특정 프로세스에서 현재 진행 중인 일만 설명합니다. AbortController, active promise, 네트워크 stream 핸들은 모두 이 계층에 속합니다. 프로세스가 끝나면 이 객체들을 직접 복원할 가치는 없습니다.
Durable Session State는 프로세스를 넘어 저장해야 합니다. 새로운 Runtime이 현재 작업 뷰를 다시 세울 수 있도록 충분한 사실을 기록해야 합니다.
따라서 에이전트를 복원할 때 더 안정적인 경로는 오래된 프로세스 내 객체를 직접 역직렬화하는 대신, 영속적인 사실에서 새 Runtime을 만드는 것입니다.
Durable Session Facts
+
Agent Definition / Version
↓
create new Runtime
↓
rebuild Context / pending work
↓
continue
이 경계는 지속성 설계에 직접적인 영향을 줍니다. 모든 메모리 필드를 직렬화하면 대개 업그레이드하기 어렵고 프로세스 간 복원도 어려운 Runtime Snapshot만 얻습니다. 더 안정적인 방법은 실행 상태를 재구성할 수 있는 사실을 저장하고, 새 Runtime이 프로세스 내 객체를 생성하게 하는 것입니다.
이 관점에서 Runtime과 Persistence는 서로 인접한 계층입니다. Runtime은 활성 상태를 관리하고 Persistence는 복원에 필요한 사실을 저장합니다. 둘은 긴밀하게 협업할 수 있지만, 같은 객체 체계를 공유할 필요는 없습니다.
Runtime은 도구 함수 모음보다 상태 기계에 가깝다
Runtime이 idle / running / waiting / cancelled 같은 상태를 갖게 되면, 많은 API의 호출 가능 여부도 현재 상태가 결정합니다.
예를 들어 Pi는 active 상태에서 prompt()를 다시 호출할 수 없지만, steer(), followUp(), abort()는 허용합니다. idle 상태에서는 새로운 Prompt를 시작할 수 있습니다.
다음과 같이 추상화할 수 있습니다.
따라서 Runtime은 데이터를 저장하는 것뿐 아니라 어떤 상태 전이가 합법적인지도 제약합니다.
이 점은 API 설계에서 중요합니다. 외부 인터페이스가 그저 여러 무상태 함수로 구성되어 있다면, 호출자가 현재 실행이 존재하는지, 어떤 입력이 어디로 들어가야 하는지, 언제 취소할 수 있는지를 직접 파악해야 합니다. 이런 제약을 Runtime 안으로 모으면 잘못된 상태를 더 일찍 드러낼 수 있고, 상위 제품에도 더 안정적인 동작 계약을 제공할 수 있습니다.
Pi가 Runtime 사례로 적합한 이유
Pi의 Agent는 하위 agent-loop.ts와 비교적 명확하게 분리되어 있습니다.
하위 Loop은 모델 요청, Tool Call, Tool Result, Steering, Follow-up의 실행을 전진시키는 역할을 합니다. 소스에서는 외곽 Agent를 low-level agent loop의 stateful wrapper라고 설명합니다.[2]
Agent가 보유한 상태는 대략 다음과 같습니다.
systemPrompt
model
thinkingLevel
tools
messages
isStreaming
streamingMessage
pendingToolCalls
errorMessage
이 필드들은 명확한 계층을 만듭니다.
messages
已经完成的稳定 transcript
streamingMessage
当前正在生成、仍会变化的 Assistant Message
pendingToolCalls
当前执行中的 Tool 状态
모든 상태가 messages에 몰려 있는 것은 아닙니다.
Pi는 Steering Queue와 Follow-up Queue도 유지합니다. Agent가 활성된 동안 prompt()를 다시 직접 호출해 두 번째 능동 실행 체인을 만드는 것은 금지되며, 새 메시지는 명시적인 입력 제어 API를 통해 현재 Runtime에 전달해야 합니다.[2]
이는 앞에서 도출한 Runtime의 특성에 이미 부합합니다. 지속 상태, 단일 활성 실행 체인, 입력 스케줄링, 취소, 수명 주기 이벤트가 모두 명확한 소유자를 갖습니다.
DeepSeek Harness는 Runtime을 한 단계 더 추상화함
DeepSeek Harness의 에이전트 추상화는 Harness 쪽에 더 가깝습니다.
공개된 Agent handle은 다음을 노출합니다.
session
inbox
status
ctx
cancel()
whenIdle()
send()
steer()
followup()
inject()
구체적인 Agent Loop은 플러그인이 제공합니다.[3]
여기에도 중요한 변화가 있습니다. 애플리케이션과 플러그인은 안정적인 Agent 인터페이스에 의존하며, 구체적인 Loop 구현은 교체할 수 있습니다.
즉, Runtime의 “실행 메커니즘” 자체도 교체할 수 있습니다. Agent Loop, Session, Tool Registry, Model Adapter를 모두 하나의 코어 클래스에 고정할 필요는 없습니다. 이 방향은 결국 Harness와 Plugin Runtime으로 이어집니다. 이 시리즈의 후속 Cordis / DeepSeek Harness 편에서 따로 다루겠습니다.
첫 번째 글에서는 판단 하나만 남기면 됩니다.
Runtime은 실행 상태의 수명 주기와 전진 문제를 해결합니다. Harness는 Runtime 기능을 어떻게 조합하고, 교체하고, 확장할지를 한 단계 더 해결합니다.
두 계층의 관심사는 서로 다릅니다.
Vercel AI SDK는 또 다른 참조점을 제공함
Vercel AI SDK의 가치는 Web 애플리케이션에서 자주 뒤섞이는 여러 계층을 분리한다는 데 있습니다.
ToolLoopAgent는 다단계 Agent 실행을 담당하고, AI SDK UI는 useChat, Transport, UI Message, 메시지 지속성, Stream Resume 등의 기능을 제공합니다.[1][6]
따라서 다음과 같이 이해할 수 있습니다.
Browser UI
│
▼
Transport
│
▼
Application / Agent Execution
│
▼
Model + Tools
AI SDK는 모든 제품이 Pi나 DSH처럼 긴 수명 주기의 Agent 객체를 사용하도록 요구하지 않습니다. 다만 UI 상태, 네트워크 전송, Agent 실행, 모델 메시지가 서로 다른 계층일 수 있다는 점을 분명한 참조로 제공합니다.
이 내용은 Web Agent Runtime을 다루는 후속 논의에서도 중요한 기반입니다.
Runtime과 Agent Framework의 경계도 관리해야 함
Runtime이 독립한 뒤에는 모든 Agent 기능을 이 계층에 다시 몰아넣기 쉬워집니다. 그러면 계속 부풀어 오르는 핵심이 다시 만들어집니다.
초기 경계로는 “현재 실행의 전진에 직접 참여하는가”를 사용할 수 있습니다.
다음 기능은 보통 Runtime과 강하게 관련됩니다.
Context preparation
Model invocation
Tool execution
Input delivery
Cancellation
Lifecycle state
이 기능들은 현재 Loop의 다음 단계가 어떻게 진행될지에 직접 영향을 줍니다.
반면 다음 기능은 Harness 또는 Application 계층에 있을 가능성이 높습니다.
Plugin discovery
Skill marketplace
Project management
Workflow definition
UI layout
Billing
Tenant permission
이 기능들은 Runtime에 Tool, Hook, Policy를 등록할 수 있지만, 하위 실행 객체의 고유 필드가 될 필요는 없습니다.
Pi와 DSH는 서로 다른 두 가지 확장 경계의 대비 사례가 될 수 있습니다. Pi는 비교적 작은 Agent Core를 유지하면서 Extension으로 동작을 확장하는 경향을 보이고, DSH는 Agent Loop, Session, Tool Registry 같은 기능 자체까지 Plugin Runtime에 넣습니다. 확장 경계는 다르지만, 둘 다 같은 문제를 해결하고 있습니다. Agent Core가 제품 기능에 따라 계속 부풀지 않게 하는 방법입니다.
따라서 Runtime 추상화도 절제되어야 합니다. 실행 수명 주기를 충분히 완전히 소유하면서, 외곽 Harness가 기능을 조합할 공간을 남겨야 합니다.
실용적인 상태 소유권 검사 방법
새 필드나 기능을 설계할 때는 다음 네 가지 질문을 차례로 확인하면 됩니다.
예를 들어 pendingToolCalls는 현재 실행 중에만 유효하고 UI 및 취소 제어에 직접 영향을 주므로 Runtime이 보유하기에 적합합니다.
Session Branch는 프로세스를 넘어 복구해야 하지만 현재 한 번의 Tool 호출을 직접 제어하지는 않으므로 Session Persistence에 두는 편이 적합합니다.
Run Status는 제품에서 독립적으로 조회하고 복구해야 하므로 Application Runtime에 둘 수 있습니다.
이런 검사 방식은 “Agent 관련 기능은 모두 Agent 클래스에 넣는다”는 접근보다 명확한 경계를 유지하기 쉽습니다.
Runtime에 Session, Run, Turn, Step이 남는 이유
실행 수명 주기를 분리하고 나도 여전히 여러 서로 다른 시간 규모가 존재합니다.
이들은 서로 다른 범위의 문제를 각각 해결합니다.
Session은 장기 세션을 관리하고, Run은 보통 애플리케이션 계층에서 한 번의 지속적 실행을, Turn은 연속된 Agent 작업의 한 구간을, Step은 한 번의 모델 의사결정과 그 결정에서 직접 발생한 Tool 작업을 의미합니다.
이 객체들은 아키텍처 그림을 더 복잡하게 만들기 위한 것이 아닙니다. 수명 주기를 하나 늘릴 때마다 대개 상위 계층이 어떤 상태를 안정적으로 표현하지 못했기 때문입니다.
예를 들면 다음과 같습니다.
두 번째 글에서는 이 추론을 이어 가며 Session, Run, Turn, Step의 경계를 세우겠습니다.
Context가 Runtime 체인에 속하는 이유
Pi는 Loop을 시작하기 전에 createContextSnapshot()을 호출합니다.
그다음 이 AgentContext를 runAgentLoop()에 전달합니다.[5]
Provider에 실제로 전달되기 전에도 Context는 transformContext와 convertToLlm을 거칠 수 있습니다.
이 구현은 자주 놓치는 경계를 보여줍니다.
Long-lived Agent State
│
▼
Context Snapshot
│
▼
Loop Local Context
│
▼
Provider Context
모델 입력은 Runtime이 특정 시점에 구성한 계산 뷰입니다.
그래서 나중에 Session을 영속화할 때 “모델이 현재 보는 것”과 “시스템이 오래 보관하는 것”을 같은 문제로 볼 수 없습니다.
시스템이 진짜 Agent Runtime을 필요로 하는지 판단하는 방법
모든 Agent 애플리케이션에 복잡한 Runtime이 필요한 것은 아닙니다.
시스템이 다음 조건을 만족한다면:
一次请求内完成
没有中途输入
没有暂停和恢复
不要求跨连接继续执行
消息列表直接作为模型上下文
Tool 状态不需要独立暴露
Tool Loop에 소량의 상태 관리만 더하면 보통 충분합니다.
요구사항이 다음처럼 변하고 나면:
이때 Runtime은 명확히 독립적인 가치를 갖습니다.
판단 기준은 상태의 수명 주기가 이미 분리되었는지에 있어야 합니다. 프레임워크 선택 자체로는 그 판단을 대신할 수 없습니다.
이 시리즈에서 채택하는 경계
후속 글에서는 다음 관계를 공통으로 사용합니다.
여기서:
- AgentDefinition은 Agent의 설정과 기능 집합을 설명합니다;
- Session은 장기 세션 사실을 저장합니다;
- Runtime은 현재 실행 상태를 관리하고 상태 변화를 전진시킵니다;
- Run은 애플리케이션 계층에서 한 번의 실행을 관리하는 경계를 제공합니다;
- Turn과 Step은 Loop 내부의 연속 작업과 모델 의사결정 경계를 설명합니다;
- Context는 모델 요청 전에 현재 상태에서 구성됩니다.
이 모델은 후속 글에서 서로 다른 프레임워크의 구현 경계를 비교하는 데 사용하며, 각 프레임워크가 동일한 API를 채택하도록 요구하지는 않습니다.
첫 번째 글에서 최종적으로 남겨야 할 판단은 하나뿐입니다.
Agent의 실행 수명 주기가 한 번의 함수 호출 또는 네트워크 요청에서 벗어나기 시작하면, 상태 소유권을 다시 나눠야 합니다. Tool Loop은 여전히 실행 커널이지만, Runtime은 독립적인 시스템 계층이 되기 시작합니다.
다음 글에서는 이 상태들의 시간 규모를 다뤄 Session, Run, Turn, Step이 왜 각각 필요한지, 어떤 경계를 Agent Core에 남기고 어떤 것을 애플리케이션 계층이 맡아야 하는지 설명하겠습니다.
참고 자료
[1] Vercel AI SDK, ToolLoopAgent: https://ai-sdk.dev/docs/reference/ai-sdk-core/tool-loop-agent
[2] Pi Agent 소스: https://github.com/badlogic/pi-mono/blob/main/packages/agent/src/agent.ts
[3] DeepSeek Harness 핵심 서브시스템: https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/subsystems/core.md
[4] DeepSeek Harness 에이전트 수명 주기: https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/agent-lifecycle.md
[5] Pi createContextSnapshot(): https://github.com/badlogic/pi-mono/blob/main/packages/agent/src/agent.ts
[6] Vercel AI SDK UI: https://ai-sdk.dev/docs/ai-sdk-ui
[7] Pi Coding Agent README: https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/README.md
[8] DeepSeek Harness 아키텍처: https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/architecture.md