AI Agent Runtime의 다음 진화: Stateless Core + Stateful Application 아키텍처

이경규·2026년 7월 15일

2025년이 “AI 에이전트를 만드는 시대”였다면, 2026년은 본격적으로 AI 에이전트를 운영하는 시대에 가깝다.

최근 해외 AI 개발 흐름을 보면 모델 자체의 성능만큼이나 반복해서 등장하는 주제가 있다.

긴 작업의 상태를 어디에 저장할 것인가
도구 사용 권한을 어떻게 제한할 것인가
에이전트가 실패하면 어디서부터 다시 시작할 것인가
사람의 승인을 어느 단계에서 받을 것인가
컨텍스트가 너무 커지는 문제를 어떻게 막을 것인가

결국 핵심은 모델이 아니다.

Agent Runtime이다.

에이전트가 실제 프로젝트 안에서 파일을 읽고, 코드를 수정하고, 테스트를 실행하고, 여러 단계의 작업을 이어가기 시작하면 단순한 프롬프트만으로는 운영할 수 없다.

모델에게 모든 진행 상황을 기억하게 만들고, 긴 대화 기록을 계속 전달하는 방식은 작업이 조금만 길어져도 한계가 온다.

그래서 최근 Agent Runtime 설계는 반대 방향으로 간다.

LLM은 가능한 Stateless하게 유지한다.

작업 상태는 Application Runtime이 관리한다.

이 구조를 이해하면 모델이 바뀌어도 유지되는 에이전트 시스템을 만들 수 있다.


1. 기존 에이전트 구조는 왜 오래 버티지 못할까

초기의 에이전트 시스템은 비교적 단순했다.

User
→ LLM
→ Tool
→ Memory
→ LLM
→ Tool
→ Memory

사용자가 요청하면 모델이 다음 행동을 판단하고, 필요한 도구를 호출한다. 도구 결과를 다시 모델에게 넣고, 작업이 끝날 때까지 이 과정을 반복한다.

작은 작업에서는 충분히 잘 동작한다.

예를 들어 다음 정도의 요청이다.

이 파일에서 컴파일 오류를 찾아줘.
이 테스트 한 개를 고쳐줘.
이 함수를 리팩토링해줘.

문제는 작업이 길어질 때 생긴다.

AI 코딩 에이전트가 다음 작업을 수행한다고 해보자.

요구사항 분석
관련 파일 탐색
수정 계획 작성
코드 변경
빌드
테스트
리뷰
실패 수정
사람 승인 대기

이때 필요한 운영 정보가 모두 대화 기록 안에 섞이기 시작한다.

현재 작업 단계
수정한 파일
실패한 명령
다음에 해야 할 일
사람 승인 여부
재시도 횟수
남은 Tool Budget
보호 파일 목록

처음에는 문제가 없어 보인다.

하지만 대화가 길어지면 컨텍스트도 함께 커진다.

컨텍스트가 커질수록 다음 문제가 나타난다.

비용 증가
응답 속도 저하
이전 지시와 현재 상태 혼동
폐기된 접근의 반복
오래된 로그와 최신 로그 충돌
작업 범위 이탈

특히 파일 수정과 테스트를 반복하는 코딩 에이전트에서는 상태를 대화 안에만 두는 구조가 금방 불안정해진다.


2. Stateless Core + Stateful Application이란 무엇인가

이 구조의 핵심은 단순하다.

LLM에게 장기 상태를 기억시키지 않는다.

모델은 현재 단계에서 필요한 정보만 받는다.

현재 목표
현재 작업 단계
읽어야 할 파일
사용 가능한 도구
만들어야 할 산출물
멈춰야 할 조건

반대로 Runtime은 다음 정보를 별도로 관리한다.

Run State
Progress
Approval
Retry
Timeout
Tool Budget
Lock
Artifacts
Policy

즉, 역할을 이렇게 나눈다.

LLM
→ 추론과 결과 생성

Application Runtime
→ 상태, 정책, 권한, 복구, 진행 관리

모델을 Stateless Core로 두면 세션이 끊겨도 문제가 없다.

상태가 모델 대화창이 아니라 Runtime에 남아 있기 때문이다.

Claude로 시작한 작업을 Codex가 이어받을 수도 있고, 다른 모델로 교체해도 현재 작업 상태를 유지할 수 있다.


3. 기본 Runtime 폴더 구조

프로젝트 안에 다음과 같은 구조를 둘 수 있다.

.ai/
├── runtime/
│   ├── state.json
│   ├── progress.json
│   ├── approvals.json
│   ├── locks.json
│   ├── retry.json
│   ├── tool-budget.json
│   └── timeout.yaml
│
├── artifacts/
│   ├── plan.json
│   ├── diff.patch
│   ├── review.md
│   └── tests.json
│
└── policies/
    ├── transition-policy.yaml
    ├── tool-policy.yaml
    ├── approval-policy.yaml
    ├── context-budget.yaml
    └── eval-gate.yaml

각 폴더의 책임은 명확하다.

runtime/
→ 현재 실행 상태

artifacts/
→ 각 단계의 결과물

policies/
→ 다음 행동과 권한을 결정하는 규칙

이 구조의 장점은 프롬프트에 모든 정보를 집어넣지 않아도 된다는 점이다.

Runtime이 필요한 정보를 읽고 현재 에이전트에게 최소한의 컨텍스트만 전달한다.


4. state.json으로 현재 실행 상태 관리하기

가장 먼저 필요한 것은 현재 Run 상태다.

{
  "run_id": "run-20260715-001",
  "phase": "implementation",
  "status": "running",
  "current_goal": "결제 중복 실행 버그 수정",
  "next_step": "review",
  "approved": false,
  "retry_count": 1,
  "max_retry": 3,
  "tool_budget": {
    "max": 10,
    "used": 4,
    "remaining": 6
  }
}

이제 모델에게 전체 대화 기록을 넣을 필요가 없다.

현재 실행 단계에 필요한 정보만 얇게 전달한다.

Current Goal:
결제 중복 실행 버그 수정

Current Phase:
implementation

Mounted Artifacts:
- plan.json
- diff.patch

Next Action:
수정 내용을 리뷰 가능한 형태로 정리

모델은 현재 목표에 집중한다.

Runtime은 현재 상태와 다음 단계를 책임진다.


5. Runtime은 상태 머신처럼 동작한다

좋은 Agent Runtime은 사실상 상태 머신이다.

Task
→ Planning
→ Implementation
→ Review
→ Testing
→ Approval
→ Completed

중요한 점은 다음 단계로 이동할지 모델이 마음대로 결정하지 않는다는 것이다.

전환 규칙은 정책 파일에 둔다.

planning:
  success: implementation
  failed: planning

implementation:
  success: review
  failed: implementation

review:
  success: testing
  failed: implementation

testing:
  success: approval
  failed: implementation

approval:
  success: completed
  rejected: review

이제 테스트가 실패하면 어디로 돌아갈지 명확하다.

Testing 실패
→ Implementation으로 복귀

Review 실패
→ Implementation으로 복귀

Approval 거절
→ Review로 복귀

AI 에이전트 운영에서 중요한 것은 실패하지 않는 시스템이 아니다.

실패했을 때 어디로 돌아가는지가 명확한 시스템이다.


6. Progress는 별도 Ledger로 관리한다

현재 상태와 작업 진행률도 분리하는 편이 좋다.

progress.json은 다음처럼 구성할 수 있다.

{
  "completed": [
    "planning",
    "implementation"
  ],
  "running": "review",
  "remaining": [
    "testing",
    "approval"
  ]
}

이 파일이 있으면 세션이 종료돼도 작업을 이어갈 수 있다.

완료된 단계
현재 실행 중인 단계
남은 단계

를 Runtime이 알고 있기 때문이다.

특정 모델의 대화 기록이 아니라 Run 자체가 유지된다.


7. Approval도 모델 기억에 맡기지 않는다

예전에는 프롬프트에 이런 문장을 넣었다.

사용자 승인 전에는 배포하지 마.

하지만 긴 작업에서는 이 지시가 컨텍스트 뒤로 밀릴 수 있다.

승인 여부는 모델이 기억하는 것이 아니라 Runtime이 관리해야 한다.

{
  "required": true,
  "status": "waiting",
  "reviewer": "human",
  "scope": [
    "package.json",
    "PaymentService.swift",
    "CheckoutViewModel.swift"
  ],
  "requested_at": "2026-07-15T09:30:00+09:00"
}

Runtime은 status가 approved가 되기 전까지 다음 단계로 넘어가지 않는다.

waiting
→ 자동 진행 중단

approved
→ 다음 단계 진행

rejected
→ review 단계 복귀

이렇게 하면 사람 승인이 실제 실행 흐름의 일부가 된다.


8. Swift로 Run State 표현하기

Runtime 상태는 특정 모델과 무관한 애플리케이션 코드로 표현할 수 있다.

enum RunPhase: String, Codable {
    case planning
    case implementation
    case review
    case testing
    case approval
    case completed
    case failed
}

struct AgentRunState: Codable {
    let runID: String
    var phase: RunPhase
    var goal: String
    var retryCount: Int
    var maxRetry: Int
    var approved: Bool
    var remainingToolBudget: Int
}

Runtime이 모델에게 전달할 컨텍스트도 별도 타입으로 정의한다.

struct RuntimeContext {
    let state: AgentRunState
    let mountedArtifacts: [String]
    let policyFiles: [String]
}

Runtime 인터페이스는 다음처럼 구성할 수 있다.

protocol AgentRuntime {
    func loadState(runID: String) throws -> AgentRunState

    func buildContext(
        for state: AgentRunState
    ) throws -> RuntimeContext

    func transition(
        _ state: AgentRunState,
        result: AgentResult
    ) throws -> AgentRunState
}

모델이 Claude인지 GPT인지 Gemini인지는 이 계층에서 중요하지 않다.

모델은 Runtime이 제공한 RuntimeContext를 받아 현재 작업만 수행한다.


9. Tool Allowlist는 Runtime이 강제해야 한다

AI 코딩 에이전트가 강해질수록 더 많은 도구를 사용한다.

파일 읽기
파일 수정
터미널 실행
패키지 설치
테스트 실행
Git 명령
배포 스크립트

모든 도구를 항상 열어두면 위험하다.

도구 정책은 최소한 세 단계로 나누는 것이 좋다.

allow
→ 자동 허용

ask
→ 사람 승인 필요

deny
→ 실행 금지

예시는 다음과 같다.

tools:
  read_file:
    mode: allow

  edit_file:
    mode: ask
    allowed_paths:
      - "Sources/**"
      - "Tests/**"
    denied_paths:
      - ".env"
      - "Secrets/**"

  run_tests:
    mode: allow

  install_package:
    mode: ask

  shell:
    mode: ask
    denied_commands:
      - "rm -rf"
      - "sudo"
      - "curl | sh"

  deploy:
    mode: deny

읽기와 테스트는 자동 허용할 수 있다.

파일 수정은 경로를 제한한다.

패키지 설치와 일반 shell 실행은 승인을 요구한다.

배포는 기본적으로 금지한다.


10. Tool Policy를 코드로 검사한다

정책 파일은 실제 실행 전에 검사돼야 한다.

enum ToolMode: String, Codable {
    case allow
    case ask
    case deny
}

struct ToolPolicy: Codable {
    let name: String
    let mode: ToolMode
    let allowedPaths: [String]
    let deniedPaths: [String]
    let deniedCommands: [String]
}

Tool 실행 요청도 구조화한다.

struct ToolRequest {
    let tool: String
    let command: String?
    let targetPath: String?
}

정책 엔진은 Tool Request를 검사한다.

enum ToolDecision {
    case allow
    case requireApproval(reason: String)
    case deny(reason: String)
}
protocol ToolPolicyEvaluating {
    func evaluate(
        request: ToolRequest
    ) -> ToolDecision
}

중요한 것은 에이전트가 Tool을 직접 실행하지 않는 구조다.

Agent
→ Tool Request
→ Runtime
→ Policy Engine
→ Tool Runner
→ Result

11. Context Budget도 Runtime의 책임이다

많은 팀이 에이전트를 만들 때 모든 자료를 컨텍스트에 넣는다.

전체 소스 코드
전체 요구사항
회의록
과거 이슈
빌드 로그
테스트 로그
이전 대화

많이 넣으면 모델이 더 잘할 것 같지만, 실제로는 중요한 정보가 묻힐 수 있다.

그래서 Runtime은 Context Budget 정책을 가져야 한다.

max_context_tokens: 60000

sections:
  system_policy:
    max_tokens: 3000
    priority: 1

  current_goal:
    max_tokens: 1000
    priority: 1

  active_artifacts:
    max_tokens: 12000
    priority: 2

  related_files:
    max_tokens: 20000
    priority: 3

  logs:
    max_tokens: 8000
    priority: 4

  history_summary:
    max_tokens: 6000
    priority: 5

overflow_strategy:
  - summarize_history
  - trim_logs
  - mount_only_changed_files
  - ask_runtime_for_more

여기서 핵심은 무엇을 넣는지가 아니다.

무엇부터 빼도 되는지를 정하는 것이다.


12. Context 우선순위를 정한다

항상 유지해야 하는 정보는 다음이다.

현재 목표
현재 단계
시스템 정책
수정 가능 범위
완료 기준

그다음은 현재 작업 산출물이다.

plan.json
현재 diff
최신 리뷰
최신 테스트 실패

낮은 우선순위는 다음이다.

오래된 로그
이전 대화
폐기된 접근
관련 없는 파일
이미 성공한 테스트 기록

좋은 Runtime은 컨텍스트를 많이 공급하는 Runtime이 아니다.

현재 단계에 필요한 정보만 공급하는 Runtime이다.


13. Artifact 중심으로 작업을 운영한다

긴 작업에서는 모델에게 “지금까지 한 일을 기억해”라고 시키면 안 된다.

각 단계의 결과물을 파일로 남긴다.

Planning 단계:

{
  "goal": "결제 중복 실행 버그 수정",
  "hypothesis": [
    "버튼 중복 탭 방지 로직이 부족함",
    "비동기 결제 요청 중 상태 잠금이 풀릴 가능성이 있음"
  ],
  "target_files": [
    "CheckoutViewModel.swift",
    "PaymentService.swift"
  ],
  "test_plan": [
    "중복 탭 시 결제 요청이 1회만 발생하는지 확인",
    "결제 실패 후 다시 결제 가능한지 확인"
  ]
}

Implementation 단계:

diff.patch
implementation.json

Review 단계:

review.md
review.json

Testing 단계:

{
  "passed": true,
  "commands": [
    "swift test"
  ],
  "failed_tests": [],
  "notes": "중복 결제 방지 테스트 통과"
}

모델이 교체되더라도 산출물은 유지된다.


14. Subagent는 역할과 권한을 함께 분리해야 한다

Subagent를 많이 만든다고 좋은 시스템이 되지는 않는다.

역할이 겹치면 책임도 흐려진다.

코딩 작업에서는 다음 정도로 나눌 수 있다.

Planner Agent
→ 요구사항 분석과 계획 작성

Implementer Agent
→ 승인된 범위의 코드 수정

Reviewer Agent
→ 변경 사항과 위험 검토

Tester Agent
→ 테스트 실행과 실패 분석

Release Guard Agent
→ 배포 가능 여부만 판단

정책 파일은 다음처럼 구성할 수 있다.

subagents:
  planner:
    allowed_tools:
      - read_file
    output:
      - plan.json

  implementer:
    allowed_tools:
      - read_file
      - edit_file
    denied_tools:
      - deploy
    output:
      - diff.patch

  reviewer:
    allowed_tools:
      - read_file
    output:
      - review.md

  tester:
    allowed_tools:
      - run_tests
    output:
      - tests.json

  release_guard:
    allowed_tools:
      - read_file
    denied_tools:
      - deploy
    output:
      - release-check.md

핵심은 에이전트마다 Tool과 산출물을 다르게 주는 것이다.


15. Release Guard가 직접 배포하면 안 되는 이유

Release Guard는 배포 가능 여부를 평가하는 역할이다.

테스트 결과 확인
미해결 리뷰 이슈 확인
보호 파일 변경 여부 확인
승인 상태 확인

하지만 실제 배포 권한은 없어야 한다.

Release Guard
→ 배포 가능 여부 판단

Human 또는 배포 시스템
→ 실제 배포 실행

평가와 실행 권한을 한 역할에 주면 자기 승인 구조가 된다.


16. Eval Gate는 마지막 단계만 있는 것이 아니다

AI 에이전트는 비결정적이다.

같은 작업도 매번 결과가 조금씩 다를 수 있다.

그래서 “이번에 잘 됐으니 괜찮다”는 방식으로 운영하면 위험하다.

Eval Gate를 둬야 한다.

gates:
  code_review:
    required: true
    checks:
      - no_secret_exposure
      - no_unapproved_file_change
      - no_large_unrelated_diff

  test:
    required: true
    commands:
      - "swift test"
    pass_condition:
      failed_tests: 0

  security:
    required: true
    checks:
      - no_env_file_access
      - no_shell_dangerous_command
      - no_dependency_without_approval

  human_approval:
    required_for:
      - package_install
      - database_migration
      - deployment

17. 단계마다 작은 Eval을 실행한다

Eval은 작업 마지막에만 실행할 필요가 없다.

Planning 단계에서는 다음을 검사한다.

수정 범위가 정의됐는가
테스트 계획이 있는가
보호 파일이 포함됐는가
중단 조건이 있는가

Implementation 단계에서는 다음을 검사한다.

승인된 파일만 수정했는가
큰 관련 없는 리팩토링이 들어갔는가
새 의존성이 추가됐는가

Review 단계에서는 다음을 검사한다.

실패 경로가 검토됐는가
민감 정보가 노출됐는가
테스트가 누락됐는가

Testing 단계에서는 다음을 검사한다.

실제 테스트가 실행됐는가
종료 코드가 기록됐는가
실패 테스트가 해결됐는가

문제를 마지막에 한 번에 잡는 것보다 앞 단계에서 차단하는 편이 낫다.


18. 실제 전체 실행 흐름

전체 흐름은 다음처럼 만들 수 있다.

1. 사용자가 작업을 요청한다.
2. Runtime이 run_id를 생성한다.
3. Planner가 plan.json을 만든다.
4. Runtime이 Context Budget에 맞춰 파일을 Mount한다.
5. Implementer가 diff.patch를 만든다.
6. Reviewer가 review.md를 만든다.
7. Tester가 tests.json을 만든다.
8. Eval Gate가 정책을 검사한다.
9. 필요한 경우 Human Approval을 기다린다.
10. 승인되면 completed 상태로 전환한다.

중간에 실패하면 transition policy를 따른다.

Review 실패
→ Implementation

Test 실패
→ Implementation

Approval 거절
→ Review

19. Runtime이 모델에 종속되면 안 된다

이 구조에서 가장 중요한 장점은 특정 모델에 묶이지 않는다는 점이다.

Claude
GPT
Codex
Gemini
사내 모델

모두 같은 Runtime 위에서 사용할 수 있다.

Provider 인터페이스를 분리하면 된다.

protocol AgentProvider {
    func execute(
        context: RuntimeContext
    ) async throws -> AgentResult
}

Runtime은 다음만 관리한다.

현재 상태
전달할 Context
실행 권한
산출물 경로
다음 단계

Provider는 모델을 호출하고 결과를 반환한다.


20. 이 구조의 실제 장점

Context가 작아진다

긴 대화 전체를 매번 넣지 않는다.

실패 복구가 쉬워진다

현재 phase와 progress를 보고 다시 시작할 수 있다.

모델 교체가 쉬워진다

작업 상태가 특정 모델 세션에 묶이지 않는다.

보안 정책을 코드로 관리할 수 있다

Tool Allowlist와 Approval Gate를 Runtime이 강제한다.

팀 단위 운영이 가능해진다

모든 단계의 산출물이 파일로 남는다.

감사와 리뷰가 쉬워진다

누가 어떤 단계에서 무엇을 만들었는지 추적할 수 있다.


21. 처음부터 거대한 Runtime을 만들 필요는 없다

최소 구성은 다음 정도면 된다.

.ai/
├── runtime/
│   ├── state.json
│   └── progress.json
├── artifacts/
│   ├── plan.json
│   └── diff.patch
└── policies/
    ├── tool-policy.yaml
    └── transition-policy.yaml

먼저 다음 흐름만 운영한다.

Planning
→ Implementation
→ Review
→ Testing
→ Human Approval

그다음 필요할 때 추가한다.

Tool Budget
Timeout
Lock
Retry
Context Budget
Eval Gate
Subagent

잘못된 프로세스를 먼저 자동화하지 않는 것이 중요하다.


22. 개발자가 왜 이 주제를 알아야 할까

AI 코딩 에이전트는 이제 단순한 코드 생성기가 아니다.

실제 파일 수정
테스트 실행
터미널 명령 실행
외부 도구 연결
패키지 변경
배포 직전 작업

까지 접근한다.

이때 모델 성능만 믿고 운영하면 위험하다.

좋은 모델보다 좋은 Runtime이 중요하다.

앞으로 개발자는 다음을 설계해야 한다.

상태를 어디에 저장할 것인가
각 단계의 완료 조건은 무엇인가
어떤 Tool을 자동 허용할 것인가
어떤 행동은 승인을 받을 것인가
실패 시 어느 단계로 돌아갈 것인가
모델에 어떤 Context만 전달할 것인가
어떤 산출물을 증거로 남길 것인가

이건 프롬프트 엔지니어링보다 애플리케이션 아키텍처에 가깝다.


23. 마무리

Agent Runtime의 다음 진화는 모델에게 더 많은 것을 기억시키는 방향이 아니다.

오히려 반대다.

LLM은 가능한 Stateless하게 둔다.

상태와 정책은 Runtime이 관리한다.

모델은 현재 목표와 필요한 자료만 받는다.

Runtime은 다음을 관리한다.

Run State
Progress
Approval
Tool Permission
Retry
Timeout
Context Budget
Artifacts
Eval Gate

이 구조를 사용하면 모델이 바뀌어도 작업은 유지된다.

세션이 종료돼도 다시 시작할 수 있다.

정책 위반은 코드로 차단할 수 있다.

사람이 개입해야 할 지점도 명확해진다.

결국 방향은 이렇다.

좋은 Prompt
→ 좋은 Agent
→ 좋은 Runtime
→ 좋은 운영

한 줄로 정리하면 다음과 같다.

LLM은 추론을 담당하고,

상태와 정책과 복구와 승인은
Application Runtime이 담당해야 한다.

모델을 잘 선택하는 개발자는 AI를 편하게 사용할 수 있다.

하지만 Runtime을 설계할 줄 아는 개발자는 AI를 실제 제품과 팀 프로세스 안에 안전하게 넣을 수 있다.

참고 자료

  • Amazon Bedrock AgentCore Runtime
  • Amazon Bedrock AgentCore Evaluations
  • Microsoft Agent Framework Workflows
  • Microsoft Agent Framework Evaluation
  • Model Context Protocol Specification
  • OpenAI Agents SDK
  • Claude Code Permissions
profile
iOS 앱 개발자

0개의 댓글