에이전트 오케스트레이션을 위한 프로그래밍 가능한 런타임 설계

@realmcore_
영어2주 전 · 2026년 7월 03일
750K
367
50
16
569

TL;DR

Onyx는 AI 에이전트를 위한 프로그래밍 가능한 런타임을 도입하여, 개발자가 TypeScript, 영속적 상태(persistent state) 및 구조화된 오류 처리를 사용하여 결정론적 시스템을 구축할 수 있도록 지원합니다.

*이 글에서는 프로그래밍 가능한 에이전트 오케스트레이션을 위한 저희의 VM 인 Onyx를 소개합니다. 나아가 오케스트레이션을 소프트웨어 엔지니어링으로 전환하는 런타임에 대해서도 다룹니다. 이 글을 다 읽고 나면 VM 구축에 들어간 제약 조건과 설계 결정, 그리고 여러분만의 프로그램을 만들고 에이전트 시스템을 설계하는 방법을 이해하게 될 것입니다.

소개

에이전트는 본질적으로 비결정적(non-deterministic)입니다. 그것이 바로 핵심입니다. 만약 결정론적( deterministic)인 결과를 원했다면, 소프트웨어를 작성했을 것입니다.

하지만 어느 순간부터 에이전트를 사용하는 모든 사람들은 더 나은 결과를 원하기 시작했습니다. 우리는 실행을 구조화된 단계(계획, 구현, 검토, QA 등)로 나누는 것이 성능 향상에 도움이 된다는 것을 알게 되었습니다. 그리고는 각 에이전트를 제어하고, 에이전트 간에 컨텍스트를 공유하고, 가드레일(guardrail)을 설정하기 위해 스크립트, 도구, 스킬을 작성하는 데 모두가 동의한 듯 보였습니다. 그런 다음 이러한 스크립트를 에이전트 간에 텍스트를 파이핑(piping)하여 연결했고, 단지 텍스트를 주고받는 방식이었기 때문에 어느 정도 작동했습니다.

이 문제에 충분한 시간을 투자하고 특히 똑똑했다면, 주어진 상태에 따라 조건부 실행이 가능하도록 시스템에서 보장(guarantee)을 얻어내는 방법을 알아냈을 것입니다. 그리고 아마 그 상태를 파싱 가능한 마크업 파일이나 파일 세트에 저장하여 배시(bash) 스크립트를 제어했을 것입니다. 어쩌면 에이전트가 사용할 맞춤형 CLI 를 직접 구축했을 수도 있습니다.

엔지니어로서 이것은 익숙한 과정입니다. 우리는 소프트웨어 엔지니어링을 하면서 스크립트를 사용합니다. 하지만 현대 소프트웨어는 배시 스크립트와 CLI 도구를 연결만 해서 만들어지지 않습니다. 대신, 시스템을 엔지니어링하는 데 도움을 주는 프로그래밍 언어, 런타임, 그리고 툴체인이 있습니다. 우리는 표준 라이브러리, 명확한 의미론(semantics), 그리고 신뢰할 수 있는 실행 모델을 제공하는 프로그래밍 언어로 소프트웨어를 작성합니다. 이러한 언어들은 우리의 모든 요구를 충족시키는 풍부한 생태계와 툴체인을 갖추고 있습니다.

시스템에 대한 보장(guarantees)은 더 높은 수준의 추상화에서 사고할 수 있게 해줍니다.

하지만 에이전트 시스템을 엔지니어링하기 위한 이와 동등한 것은 없습니다. 시스템을 구축하려면 에이전트 오케스트레이션이 현대 소프트웨어와 정확히 동일한 방식으로 프로그래밍 가능(programmable)해야 합니다.

오늘 우리는 PROGRAMS (*.program.ts) 의 스펙과 결정론적 에이전트 오케스트레이션을 위해 구축된 저희의 VM 인 Onyx 를 소개합니다. 이 글에서는 에이전트 오케스트레이션의 역사, 프로그램을 실행할 수 있는 VM 의 정적 및 런타임 의미론, 그리고 이것이 이 분야의 미래에 시사하는 바를 살펴봅니다.

비용이 많이 들 것 같지만, 실제로는 그렇지 않습니다. 이에 대해서는 글 후반부에서 설명하겠습니다.

궁금하신 분들을 위해 말씀드리자면, 이것은 Andrej Karpathy 의 Autoresearch 를 프로그램으로 표현한 것입니다.

akira - inline image

에이전트 오케스트레이션의 미해결 문제

에이전트 오케스트레이션을 위한 런타임이 무엇을 포함해야 하는지 이해하려면 먼저 에이전트의 한계를 이해해야 합니다.

LLM 에이전트는 JSON 스트림 생성기로 생각할 수 있습니다. 이 생성기는 파서(parser)에 입력되고, 파서는 도구 호출을 환경에 반복적으로 전달합니다.

모든 도구 호출은 동일한 외부 스키마 형태를 가지지만, 이 출력 스트림의 콘텐츠결정론적이지 않습니다.

결정론과 비결정론의 조합이 에이전트를 매우 가치 있게 만든 이유입니다. 에이전트는 독특한 방식으로 일련의 동작을 연결할 수 있을 정도로 유연하면서도, 도구 호출을 통해 컴퓨터와 상호 작용할 수 있을 정도로 결정론적입니다.

해당 스트림의 콘텐츠에 타입을 지정해야 한다는 요구 사항을 포기할 의향이 있다면, 합성 가능성(composability)은 거의 공짜에 가깝습니다. 모델은 우리가 제공하는 레일(프롬프트, 메시지, 도구 호출) 내에서 텍스트를 주고받을 수 있을 만큼 충분히 좋습니다.

이는 매우 합성 가능한 인터페이스, 즉 텍스트를 드러냅니다.

텍스트는 보편적인 인터페이스입니다. 컴퓨터의 모든 것은 기계 코드일지라도 텍스트로 직렬화(serialize)될 수 있습니다. LLM 이 이 보편적인 인터페이스를 통해 텍스트를 입력 및 출력할 수 있다면, 텍스트 스트림에 대한 합성성을 얻을 수 있습니다.

이는 에이전트 동작의 신뢰성이 모델의 출력 일관성과 직접적으로 관련된다는 것을 의미합니다. 출력 변동성이 크면 에이전트 동작이 더 불규칙해집니다.

조각들을 연결할 인터페이스를 확보했다면, 다음으로 고려해야 할 제약 조건은 조종 가능성(steerability) 입니다.

에이전트가 수행하기를 원하는 작업과, 원하는 작업을 일관되게 수행하도록 만드는 방법

우리는 에이전트가 샘플링하는 분포를 이동시켜, 즉 프롬프팅(prompting)을 통해 에이전트를 조종합니다.

2022년, ReAct 가 등장하여 에이전트 조종 가능성의 시대를 사실상 개척했습니다. 사실, ReAct 가 우리가 아는 에이전트라는 개념을 만들었다고 해도 과언이 아닙니다. 다음 단계를 수행하기 전에 도구의 출력에 대해 생각하고 추론하는 과정이 루프의 일관성을 유지하는 핵심입니다.

akira - inline image

우리는 여전히 에이전트가 더 똑똑해지기를 원했습니다. @OpenAI 의 O-시리즈 모델에 의해 생산화된 테스트 시간 연산 확장(test time compute scaling)의 사용은 모델 연구소들이 더 나은 에이전트 동작을 내장할 수 있게 해주었습니다 [[11]](http://localhost:5173/blog/onyx#ref-11). 도구를 호출하기 전에 더 많은 토큰을 출력함으로써 모델은 추론 출력 길이에 제약을 받았을 때 갇혀 있었을 출력 분포에서 벗어날 수 있습니다. 모델이 출력 분포 공간을 탐색하는 방식을 훈련하도록 선택할 수 있으며, 따라서 관심 있는 작업에 대해 더 명확한 에이전트 행동을 훈련할 자유를 가질 수 있습니다.

컨텍스트 길이가 제한 없이 증가함에 따라 에이전트를 조종하는 것은 어려워지고 작업 완료 가능성은 낮아집니다. 추론 모델을 사용하더라도 복구가 보장되지 않으며, 에이전트는 그 자리에서 멈출 수 있습니다. 에이전트는 컨텍스트 한도에 도달하거나, 조기에 완료를 선언하거나, 루프에 갇히는 등의 문제가 발생할 수 있습니다.

비결정적 시스템에서 보장 끌어내기

이에 대한 해결책은 다양했지만, 그중 하나가 특히 돋보입니다: The Ralph Loop , @GeoffreyHuntley 가 만들었습니다. [[3]](http://localhost:5173/blog/onyx#ref-3

그는 에이전트 실행을 제한(bound)하고, 그 제한을 사용하여 작업 완료에 대해 추론할 수 있다는 아이디어를 소개했습니다. 이를 통해 Ralph Loop 는 마법 같은 일을 할 수 있게 되었습니다. 비결정적 시스템에서 의지할 수 있는 무언가를 제공하는 것입니다.

결정론의 불꽃.

슬롯 머신 레버를 한 번 더 당기는 것보다는, 실패를 보장하고 점진적으로 올바른 방향으로 나아가는 것이 훨씬 낫습니다. 이렇게 정의된 경계는 추론할 수 있는 구체적인 대상을 제공하고, 일단 무언가의 경계에 대해 추론할 수 있게 되면 그것을 바탕으로 시스템을 만들 수 있습니다.

컨텍스트 길이의 한계와 싸우기

하지만 문제가 있습니다. 새로운 에이전트는 실행 간에 일관성을 잃기 쉽고, 단일 에이전트는 시간이 지남에 따라 컨텍스트가 부족해집니다.

여기서 RLM 이 등장합니다. @lateinteraction@a1zhang 의 RLM 은 긴 컨텍스트(즉, 에이전트 실행)와 구조화된 방식으로 상호 작용하는 방법에 대한 개념을 제공했습니다 [[4]](http://localhost:5173/blog/onyx#ref-4). RLM 은 2024년 논문인 CodeAct 에서 영감을 받았는데, 해당 논문은 코드를 사용하여 연산(operations)을 오케스트레이션하는 방법을 보여주었습니다 [[5]](http://localhost:5173/blog/onyx#ref-5). 에이전트는 REPL 내에서 연산을 오케스트레이션하는 스크립트를 작성하여 출력을 검색합니다. RLM 은 동일한 방식으로 작동하지만, 변수를 사용하여 컨텍스트를 저장하고 해당 컨텍스트에 대해 연산을 수행한다는 추가적인 특징이 있습니다. 또한 REPL 에서 재귀적 LLM 호출을 허용합니다. 다른 루프가 가진 반응성을 일부 잃을 수 있지만, 컨텍스트를 프로그래밍 방식으로 다룰 수 있는 능력을 얻게 됩니다. 여기서 핵심은 REPL 의 스크립트가 일시적(ephemeral)이라는 것입니다. 스크립팅 런타임과 컨텍스트 관리를 얻지만, 재사용성이나 합성성은 없습니다. 그냥 스크립트를 작성하고 실행하면 사라집니다. 시스템 구축 측면에서 보면, 지속성(persistence)과 제한된 실행(bounded execution)을 잃기 때문에 에이전트와 마크다운 파일을 배시 스크립트로 연결하는 것보다 엄격하게 나쁩니다.

개별 루프에서 오케스트레이션 확장으로

OpenAI 의 Deep research[[6]](http://localhost:5173/blog/onyx#ref-6) 는 결정론적 워크플로우의 초기 사례 중 하나였습니다. 이는 일반적인 실행 형태 또는 스키마를 가지며 실행마다 약간의 변동성만 있었습니다. 작동 방식은 쿼리 배치를 계획하고, 웹에서 실행하고, 결과를 검토한 후 다음 쿼리 배치를 계획하는 것입니다. 각 배치는 문제 공간을 더 깊이 탐구합니다.

akira - inline image

Cursor 는 @wilsonzlin 이 브라우저를 구축하기 위해 에이전트를 오케스트레이션하는 하네스(harness)를 선보이면서 결정론이라는 아이디어를 훨씬 더 발전시켰습니다. 그는 병렬 계획 에이전트와 작업 에이전트를 사용하여 대량의 작업을 조정하는 맞춤형 하네스를 구축했습니다 [[7]](http://localhost:5173/blog/onyx#ref-7). 여기서 중요한 것은 하네스의 각 부분 간의 관계가 고정되어 있다는 것입니다. 현재 시스템 상태를 탐색하고 작업을 생성하는 계획자(planner)가 있고, 작업을 받아 병렬로 구현하는 실행자(executor)가 있습니다. 에이전트 간에는 고정된 가드레일이 있고, 정보를 전달하기 위한 고정된 채널이 있습니다. 조정을 잘 수행하려면 인터페이스에 대한 보장이 필요합니다.

종료 조건을 사용한 제한된 실행

5월, Codex 는 목표(goal)라는 아이디어를 도입했습니다. 이는 원하는 최종 상태를 향해 작업이 완료될 때까지 언덕 오르기(hillclimb)를 수행하는 검증기(verifier) 루프를 사용합니다. 이것은 Ralph Loop 의 프로덕션 준비 버전으로, Codex 에 내장되어 있습니다. 목표를 지정할 수 있게 해주며, 실행 및 검토를 자동화하는 루프가 내장되어 있습니다.

akira - inline image

Karpathy 의 autoresearch[[9]](http://localhost:5173/blog/onyx#ref-9) 는 Codex 의 /goal 및 Ralph Loop 와 유사합니다. 검증 가능한 종료 조건과 반복에 대한 실행 제한을 결합하여 목표를 향해 지속적으로 나아갈 수 있게 합니다. 아이디어 공간을 검색하여 시간이 지남에 따라 반복적으로 개선함으로써 진전을 이룹니다.

akira - inline image

지금까지 에이전트 외부로 오케스트레이션을 외부화하는 모든 솔루션은 실행 그래프 형태가 고정되어 있습니다. 손으로 작성한 패턴을 사용하여 실행되며, 작동 가능한 허용 형태에 대한 일종의 스키마를 가지고 있습니다. 작업별로 적응하지 않거나 실행 그래프 형태 자체에 대한 강력한 보장을 제공하지 않습니다.

오케스트레이션을 유연하게 만들기

올해 3월, 우리는 RLM 스타일의 실시간 서브에이전트 오케스트레이션을 위해 코드를 사용하는 최초의 코딩 에이전트인 Slate 를 소개했습니다. Slate 는 여전히 코드를 사용하여 실시간 에이전트 오케스트레이션을 수행하는 유일하게 잘 사용되는 코딩 에이전트입니다. Slate 에서 스레드는 실시간으로 생성, 일시 중지, 재개 및 조종될 수 있습니다. 메인 에이전트는 실행 중인 모든 서브에이전트를 오케스트레이션하는 방법을 깊이 이해하고 있어 사용자가 직접 할 필요가 없습니다. 하지만 RLM 과 유사하게, 우리는 여전히 서브에이전트 간 상태 공유와 일시적인 스크립팅이라는 문제에 직면했습니다. 이는 배시 스크립트와 마크다운 파일을 사용할 때는 발생하지 않는 문제입니다.

그럼에도 불구하고, 모델이 오케스트레이션을 수행한다면 어떻게 그것을 조종할까요? 오케스트레이션 코드를 특정 방식으로 작성하라고 지시해야 할까요? 어떻게 해야 할까요?

이에 대한 초기 해결책(Onyx 런타임을 출시하기 전 패치 형태)은 오케스트레이션 스킬(orchestration skills) 이라고 불렸습니다 [[13]](http://localhost:5173/blog/onyx#ref-13). 아이디어는 간단했습니다. 사용자가 에이전트가 오케스트레이션에 접근하는 방식을 제어하는 스킬을 제공할 수 있도록 하는 것입니다. 그게 전부였습니다. 어느 정도 작동했지만 많은 문제가 있었습니다.

즉, 스킬은 구속력 있는 행동 계약이 아닙니다. 텍스트로부터 보장을 얻을 수 없습니다.

이는 오케스트레이터가 원하는 실행 패턴을 따를 필요가 없었음을 의미합니다. 이를 강제할 실제 방법이 없었기 때문입니다. Onyx 런타임의 가장 큰 장점 중 하나는 이 문제를 해결했다는 것입니다.

앞서 언급된 어떤 시스템도 구속력 있는 행동 계약을 가지고 있지 않습니다.

그렇다면 에이전트가 작업별로 오케스트레이션 코드를 스크립트에 직접 작성하여 실행 그래프를 고정하면 어떨까요? 이것이 바로 Claude 동적 워크플로우(dynamic workflows) 입니다.[[10]]([http://localhost:5173/blog/onyx#ref-10)[[12]](http://localhost:5173/blog/onyx#ref-12](http://localhost:5173/blog/onyx#ref-10)[[12]](http://localhost:5173/blog/onyx#ref-12) RLM 및 Slate 와 같은 방식으로, 서브에이전트를 오케스트레이션하는 코드를 작성함으로써 동적 워크플로우는 Claude 가 워크플로우 형태를 작성하고 저장할 수 있게 합니다. 이는 /loop 와 결합하여 특정 패턴을 반복할 수 있습니다. 에이전트 집합의 동작에 대한 선언적 계약을 제공합니다. 하지만 기능적 합성(functional composition)과 같은 것들이 부족하여 소프트웨어를 작성하는 것과는 여전히 다릅니다. 그러나 지속성과 작업이 실행될 방식에 대한 강력한 보장을 얻을 수 있습니다. 이들은 특정 작업을 위해 임시로 동적으로 작성된 워크플로우 스크립트입니다.[[12]](http://localhost:5173/blog/onyx#ref-12) 그리고 디스크에 저장되기 때문에 한 가지 추가적인 이점이 있습니다: 다시 실행하거나 /loop 와 같은 오케스트레이션 접착제로 래핑할 수 있다는 점입니다.

akira - inline image

눈치채셨겠지만, 위의 모든 솔루션은 동일한 목표를 추구하고 있습니다: 시간이 지남에 따라 에이전트가 어떻게 실행될지 제어하는 결정론적 방법입니다.

이것은 우리가 이미 소프트웨어 엔지니어링 분야에서 목격한 이야기입니다. 우리는 서로 다른 시스템을 연결하고 작업을 스크립팅하는 것에서 시작하여, 언어가 더 유연하고 강력해졌습니다. 더 강력한 생태계를 통해 엔지니어링 프로세스에 대한 leverage 를 점점 더 확보하여 더 높은 추상화 수준에서 더 안정적인 시스템을 구축할 수 있게 되었습니다.

현재 에이전트도 같은 궤적을 따르고 있으며, 오늘 우리는 그 궤적의 다음 단계를 공개하여 여러분이 에이전트를 실행하는 시스템을 엔지니어링할 수 있도록 합니다. 프로그래밍 언어는 종종 인터프리터나 VM 을 사용하여 리소스를 자동으로 스케줄링합니다. 이것이 언어를 사용하는 엔지니어에게 leverage 를 제공하는 방법입니다.

VM 이 에이전트 오케스트레이션에 적합하려면 몇 가지가 필요합니다:

  1. 영구 상태 관리: 상태를 정의하고, 이름으로 참조하고, 유지하고, 프로그래밍 방식으로 조작할 수 있어야 합니다.
  1. 타입 보장: 정의된 입력 및 출력 형태를 존중하고 따라야 하며, 이를 신뢰할 수 있어야 합니다.
  1. 제어 흐름 기본 요소: 가급적 LLM 이 이해할 수 있는 잘 알려진 기본 요소여야 합니다.
  1. 명확한 오류 처리 구조: (예: try-catch)
  1. 리소스 관리: 에이전트 병렬 처리, 비용, 실행 중인 모델 등 리소스에 대한 정의된 제어가 필요합니다.
  1. 실행 격리: 상태가 명시적으로 공유되지 않는 한, 실행 중인 특정 에이전트나 프로그램은 다른 것과 격리되어야 합니다.
  1. 수명 주기 제어: 에이전트 프로그램의 형태와 실행, 취소, 조종에 대한 의미론이 필요합니다. 이것이 없으면 정리(cleanup) 경로가 명확하지 않고 수명 주기 관리를 제어할 수 없습니다.
  1. 합성성: 프로그램은 서로 합성될 수 있어야 하며 정의된 입력 및 출력 타입으로 호출 가능해야 합니다.
  1. 가시성: 무엇이 언제 실행되었는지 알 수 있어야 하고, 실행 실패를 소스에서 추적할 수 있어야 합니다.
  1. 내구성: 충돌에서 복구하고 재개할 수 있는 명확한 모델이 있어야 합니다.

이 모든 문제는 수십 년 전에 이미 프로그래밍 언어에 의해 해결되었습니다. 에이전트 오케스트레이션은 이제 막 이러한 모든 문제를 다시 마주하고 있는 것입니다.

이를 위해 진정으로 소프트웨어를 작성하려면, "program.ts" 프로그램은 위의 모든 것을 지원하는 런타임에서 작성되어야 합니다. 그래야 프로그램이 작동하지 않을 때 어떤 일이 일어날지 추론하고 실패에 대비한 엔지니어링을 할 수 있습니다.

이것이 우리가 Onyx 를 구축한 이유입니다. Onyx 는 영구적이고 합성 가능한 프로그램과 해석된 스크립팅 계층을 모두 지원하도록 정밀하게 설계된 에이전트 오케스트레이션 VM 입니다. 이것이 작동 방식과 "program.ts" 호환 런타임이 지원해야 하는 내용입니다.

런타임 설계

언어와 해당 언어의 런타임을 설계할 때, 우리는 추론할 수 있기를 원하는 제약 조건과 쉽게 표현할 수 있기를 원하는 것이 무엇인지 고려해야 합니다. 그런 다음 결과 의미론을 정적 의미론(static semantics)과 런타임 의미론(runtime semantics)의 두 가지 범주로 나눌 수 있습니다.

정적 의미론은 프로그램을 보는 것만으로 추론할 수 있는 모든 것입니다. 컴파일러나 타입 검사기가 특정 프로그램에 대해 알고 있는 것들입니다.

런타임 의미론은 코드가 실제로 의미하는 것과 프로그램이 실제로 실행되는 방식을 정의합니다. 여기에는 기본 리소스 할당 및 스케줄링 메커니즘이 포함됩니다.

에이전트를 위한 런타임의 목표는 오케스트레이션 제어 흐름을 코드로 전환하고, 실행 상태를 영구적이고 타입이 지정된 상태로 만들어 오케스트레이션을 안정적으로 조종하는 데 사용할 수 있도록 하는 것입니다.

VM 의 몇 가지 요구 사항

일반적인 TypeScript 실행과는 달리 VM 에 대해 우리가 특히 중요하게 생각하는 3가지 사항이 있습니다.

  1. 에이전트 오케스트레이션 런타임으로서 에이전트를 오케스트레이션할 수 있어야 합니다. 즉, 에이전트를 생성하고, 수명 주기를 추적하는 등의 작업이 필요합니다. 런타임이 에이전트를 블로킹(blocking) 또는 논블로킹(non-blocking) 방식으로 실행하고 올바르게 스케줄링할 수 있기를 원합니다.
  1. 에이전트의 출력 형태를 제어하고 엄격한 출력 계약을 적용하기를 원합니다.
  1. 모델 및 비용과 같은 외부 리소스에 대한 런타임 제어를 원합니다.

에이전트 및 프로그램 실행

에이전트를 실행하기 위해 우리는 두 가지 기본 동사를 선택했습니다: runspawn. run 은 블로킹 에이전트를 포그라운드에서 실행합니다. spawn 은 에이전트를 백그라운드에서 실행합니다. 이는 posix_spawn 과 같은 일반적인 spawn 의 이해와 일치하므로 모델이 새로운 동사를 이해하기 쉽습니다. 왜냐하면 개념적으로 훈련 데이터에 있기 때문입니다. spawnrun 은 디스크에서 읽은 에이전트와 프로그램을 직접 호출하여 실행 핸들에 충분한 정보를 반환합니다.

run 은 몇 가지를 더 지원합니다. zod @colinhacks 를 통해 직접 적용되는 출력 타입을 지원하고, 직접 모델 오버라이드를 지원하여 여러 다른 모델로 다양한 솔루션이나 작업 단계에 분산(fan out)하는 프로그램을 쉽게 작성하고 실행할 수 있습니다.

typescript
1function run<S extends z.ZodType>(
2 name: string,
3 options: ...
4): Promise<z.infer<S>>

run을 사용하면 서브에이전트를 인라인으로 직접 연결할 수 있습니다.

typescript
1// 일반 에이전트 실행
2const out = await run({ type: "read", prompt: () => "Reply with: ok" })
3// 이름이 지정된 실행 (string = 하위 워크플로우 ID)
4const review = await run("reviewer", {
5 type: "general",
6 prompt: () => "Review the diff",
7})
8// 구조화된 출력 (타입이 지정된 결과)
9const Verdict = z.object({ risk: z.enum(["low", "high"]), why: z.string() })
10const v = await run({
11 type: "general",
12 prompt: () => "Assess risk",
13 output: Verdict,
14})

spawnrun 과 유사하지만 백그라운드에서 에이전트를 생성합니다. spawn 된 서브에이전트는 대기되지 않으며 제어 흐름은 계속 진행됩니다. spawn 은 여러 개의 논블로킹 실행 에이전트를 생성하는 데 매우 유용합니다.

typescript
1// 백그라운드 에이전트
2const h = await spawn("worker", { type: "general", prompt: "Long task" })

실행 중인 에이전트와 상호 작용

실행 중인 에이전트에 대해 두 가지 유형의 작업을 수행할 수 있기를 원합니다: 조종(steering)과 중지(stopping)입니다.

조종 메시지는 에이전트가 실행되는 동안 LLM 이 수신하여 특정 방향으로 유도하는 메시지입니다. 이는 작업자를 종료할 필요 없이 에이전트의 작업 컨텍스트를 업데이트하는 데 유용합니다.

취소(cancellation)도 중요합니다. 실행 중이어서는 안 되는 서브에이전트를 적극적으로 종료할 수 있어야 합니다.

라이브 REPL 과 사전 작성된 프로그램 모두에서 이러한 작업을 실행할 수 있는 능력은 Slate 에 모든 것을 실시간으로 오케스트레이션할 수 있는 능력을 부여합니다. Slate 는 런타임에 오케스트레이션의 형태를 동적으로 정의하거나, 오케스트레이션을 수행하기 위해 실제 소프트웨어를 작성하고 반복할 수 있습니다.

Slate 는 \.program.ts 파일에 프로그램을 작성할 수 있습니다. 프로그램 파일에는 몇 가지가 포함됩니다: 이름( Slate 가 이것이 무엇인지 아는 방법), JSDoc 설명, 그리고 실제 프로그램 본문*. 프로그램 선언은 다음과 같습니다:

typescript
1program(async (ctx) => {
2 // 검색을 위한 저렴한 모델 - 파일을 찾기만 하면 됨
3 const findings = await run("search", {
4 type: "read",
5 prompt: "Find all authentication-related files",
6 model: "codex/gpt-4.1-mini", // 내장된 codex 키 사용
7 })
8})

프로그램은 동일한 비동기(async) 실행 모델을 따르므로 포그라운드와 백그라운드 모두에서 프로그램을 실행하고 실행 중에 상호 작용할 수 있습니다.

typescript
1// 백그라운드 에이전트
2const h = await spawn("worker", { type: "general", prompt: "Long task" })
3await h.notify("focus on the parser first") // 실행 중인 에이전트에 조종 메시지 전송
4const result = await h.result() // 나중에 완료 대기
5
6// 분산 후 수집
7const a = await spawn({ prompt: "task A" })
8const b = await spawn({ prompt: "task B" })
9const [ra, rb] = [await a.result(), await b.result()]
10
11// 백그라운드 프로그램
12import Audit from "deep-audit"
13const ah = await spawn(Audit, { input: { pr: 42 } })
14const auditResult = await ah.result()

구조화된 출력과 상태

이것은 지금까지의 다른 모든 시스템의 주요 한계입니다. 다른 모든 시스템에서 상태는 제대로 외부화되지 않았고 안전하게 격리되지 않습니다. 시스템의 파일이라면 손상이 없음을 보장할 수 없습니다. 보장할 수 있다 하더라도 파싱 가능성을 보장할 수 없습니다. 상태 변경을 구독하여 작업을 수행할 수 없으며, 타입 준수를 보장할 수 없습니다.

우리가 원했던 영구적이면서도 구조화되고 참조 가능한 상태를 기억하시나요?

Onyx 에서 상태는 다릅니다. 상태 네임스페이스는 선언되고, 직접 이름이 지정되며 시간이 지남에 따라 유지됩니다. 즉, 상태 저장소를 계속해서 재사용할 수 있어 실제 데이터로 장기 실행 에이전트 시스템을 구축할 수 있습니다.

에이전트와 코드 모두 상태를 읽으며, 우리가 원했던 런타임의 결정론은 이로부터 비롯됩니다. 에이전트는 전용 도구를 통해 상태를 읽어 항상 안전하게 구조화된 방식으로 상호 작용할 수 있습니다. 에이전트와 프로그램 모두 상태를 수정하도록 조종될 수 있는 소비자이며, 이를 통해 런타임은 상태 객체에 의존하여 오케스트레이션을 수행할 수 있습니다.

상태와 스키마 준수는 서브에이전트 완료를 제어(gate)합니다. 이 때문에 상태는 전체 프로그램을 조종하기 위한 통합된 표면을 제공합니다.

상태 객체는 런타임 변수로 메인 에이전트와 공유되는 하위 세션에 전달될 수도 있습니다. 에이전트 계층 전체에 걸친 이 참조에 의한 전달(pass by reference) 접근 방식(그 자체로 최초)은 공유 상태 채널을 통한 에이전트 간 통신을 가능하게 합니다.

akira - inline image

장기 실행 루프

일부 프로그램은 실행 중인 시스템처럼 기능해야 합니다. 예를 들어 openclaw 를 생각해 보십시오. 적절한 기본 요소가 주어지면 openclaw 를 프로그램으로 표현할 수 있습니다. 이를 위해 우리는 두 가지 기본 요소를 사용합니다: sleepcheckpoint.

sleep 은 예상대로 작동합니다. 잠자기 상태가 됩니다.

자, 여기서 중요한 점은 백그라운드에서 장기 실행 작업 관리를 원한다고 가정해 보겠습니다. 미리 정의된 실행 그래프는 막히거나 깨질 수 있으므로 메인 에이전트가 프로그램의 상태를 인식하는 것이 중요합니다.

이를 지원하기 위해 checkpoint 기본 요소를 도입합니다.

checkpoint 는 무엇이든 될 수 있지만, checkpoint 라고 명명된 이유는 고정된 형태의 객체로 메인 에이전트에 알리기(notify) 때문입니다. 이를 통해 메인 에이전트는 작업 진행 상황과 같은 것을 추적하고 프로그램 상태의 변경 사항을 직접 통보받을 수 있습니다. 결과적으로 메인 에이전트는 실행 중인 프로그램을 더 효과적으로 관리할 수 있습니다.

Onyx 는 Openclaw 와 같은 에이전트 루프, 즉 하트비트(heartbeat)를 가진 영구 에이전트를 만드는 것을 지원합니다.

이것은 정말 멋집니다. while 루프, sleep, 체크포인트만 사용하여 프리미티브를 완전히 다른 유형의 에이전트로 구성할 수 있습니다.

Openclaw는 단순히 프로그램 파일로 표현될 수 있습니다!

typescript
1// 장기 실행 자동 연구 스타일 루프를 실행하는 프로그램
2for (let i = 0; i < maxExperiments; i++) {
3 const idea = await run("propose", { ... })
4 const result = await run("train", { ... })
5 checkpoint({ message: `experiment ${i}`, data: { idea, result } })
6 await sleep(30_000) // 실험 간 쿨다운
7}
8
9// openclaw 스타일의 지속적 에이전트를 실행하는 프로그램
10while(true) {
11 const status = await run("status_check", { ...insert cheap model here... })if(status.pending_tasks) {checkpoint({ tasks: status.pending_tasks }) // 중요한 상태를 반환하고 메인 에이전트를 깨움}
12 await sleep(30_000) // 실험 간 쿨다운
13}

구성

Onyx를 사용하면 Slate가 *.program.ts 파일을 작성할 수 있습니다. 이 파일은 지속성을 가지며 일반 코드처럼 취급되어야 합니다. 기본 제공되는 타입이 있고, 런타임 전역 변수가 제거된 환경에서 실행되며, TypeScript이기 때문에 다른 프로그램을 가져와서 호출하는 방식으로 구성됩니다.

TypeScript이기 때문에 병렬 처리(Promise.all)와 루프를 무료로 얻을 수 있습니다.

한 프로그램을 가져와서 다른 프로그램에서 사용하는 방법은 다음과 같습니다:

typescript
1import Audit from "deep-audit"program (() => {const ah = await spawn(Audit, { input: { pr: 42 } })
2 const auditResult = await ah.result()
3 const fixer = await run("fixer", ... audit output) // 이는 실행되어 감사 프로그램 출력을 수정합니다.
4})

오류 의미론

이상적인 VM에서 오류는 명시적으로 발생합니다. 이러한 오류는 런타임 구문 문제, 에이전트 실패, 충돌 등에서 발생해야 합니다.

구체적으로, 오케스트레이션 오류는 다음과 같이 정의합니다:

  • 에이전트가 작업에 막혔습니다.
  • 에이전트가 작업을 완료하지 못했습니다.
  • 에이전트가 작업에 대한 단계 또는 예산을 소진했습니다.
  • 프로그램이 실행 예산을 소진했습니다.
  • 오케스트레이션 모델이 구문적으로 올바른 코드를 작성하지 못했습니다.
  • 불법적인 상태 수정이 이루어졌습니다.

이러한 모든 특정 오류 사례는 런타임 의미론을 정의합니다. 즉, "코드 오류를 보는 것과 동일하게 에이전트 실행 실패를 보기 때문에 이 런타임이 오류를 발생시킬 것으로 예상할 수 있습니다"라는 의미입니다. 처음에는 성가실 수 있지만, 이 명시적인 실패 메커니즘은 대가를 제공합니다: 실패에 대비하고 프로그래밍할 수 있는 명시적인 방법입니다. 따라서 실제로는 통제력을 덜 빼앗는 것이 아니라 더 많이 제공합니다.

typescript
1// 오류는 try/catch로 처리됩니다 — 다른 TypeScript 프로그램과 동일합니다.
2program(async (ctx) => {
3 try {
4 const result = await run("risky-refactor", {
5 type: "general",
6 prompt: "Refactor the auth module",
7 model: "claude-sonnet",
8 maxSteps: 20,
9 })
10 } catch (err) {
11 // 에이전트가 실패했습니다 — 하지만 정확히 이유를 알고 있습니다.
12 // 추적에는 여기까지 이어진 모든 도구 호출, 모든 모델 요청,
13 // 모든 상태 쓰기가 포함되어 있습니다.
14
15 // 다른 모델로 재시도
16 const result = await run("risky-refactor-retry", {
17 type: "general",
18 prompt: `The previous attempt failed: ${err.message}. Try a different approach.`,
19 model: "claude-opus",
20 maxSteps: 30,
21 })
22 }
23})

모델 선택, 예산 관리 및 BYOK

모델 선택이 내장되어 있으면 더욱 정밀한 제어가 가능합니다. /models 스킬은 Slate에 사용 가능한 모델 목록에 대한 전체 액세스 권한을 제공하여 Slate가 다양한 작업을 수행하는 여러 모델로 프로그램을 작성할 수 있게 합니다. Fable을 플래너로 사용하고 GLM 5.2를 결정론적 하네스 내에서 구현하도록 하시겠습니까? 물론 가능합니다. 질문을 Gemini, GPT 5.5 및 DeepSeek에 분산시키시겠습니까? 그것도 가능합니다.

또한 런타임은 프로그램에 대해 두 가지 유형의 구성 재정의를 지원합니다:

  • 에이전트 실행에 사용되는 기본 글로벌 모델
  • 프로그램 실행 예산

주어진 루프의 지출을 제한하기 위해 실행 예산을 직접 설정할 수 있습니다.

또한 런타임은 기존 OpenAI 및 Github Copilot 구독 사용을 지원합니다.

typescript
1program(async (ctx) => {
2 // 검색용 저렴한 모델 — 파일만 찾으면 됩니다.
3 const findings = await run("search", {
4 type: "read",
5 prompt: "Find all authentication-related files",
6 model: "codex/gpt-4.1-mini", // 내장된 codex 키를 사용합니다.
7 })
8
9 // 어려운 부분을 위한 추론 모델 — 생각이 필요합니다.
10 const plan = await run("architect", {
11 type: "general",
12 prompt: `Design a fix based on: ${findings.output}`,
13 model: "openai/o3", // API 크레딧을 사용합니다.
14 output: z.object({
15 approach: z.string(),
16 files: z.array(z.string()),
17 risk: z.enum(["low", "medium", "high"]),
18 }),
19 })
20
21 // 구현을 위한 중간급 모델 — 편집만 하면 됩니다.
22 const handles = await Promise.all(
23 plan.files.map(f => spawn("fix-" + f, {
24 type: "general",
25 prompt: `Apply this fix to ${f}: ${plan.approach}`,
26 model: "anthropic/claude-sonnet-5",
27 maxSteps: 15,
28 }))
29 )
30 await Promise.all(handles.map(h => h.result()))
31})

제작 표면 정의

프로그램의 제작 표면을 설계할 때 두 가지 주요 요소가 있었습니다: 에이전트가 얼마나 쉽게 이해할 수 있는지, 그리고 사람이 얼마나 쉽게 읽을 수 있는지입니다. 우리는 영어처럼 읽히는 비교적 간단한 동사를 선택했으며, 오케스트레이션을 선언적이 아닌 절차적으로 모델링하기로 명시적으로 결정했습니다.

언어로 TypeScript를 선택한 것도 중요했습니다. 실제 환경에는 절차적 TypeScript 코드가 너무 많기 때문에 모델은 사후 훈련 없이도 TypeScript 의미론을 암시적으로 이해할 것입니다.

akira - inline image

소프트웨어 팩토리의 엔지니어링 부품

다음으로 답해야 할 질문은: 이 모든 것이 무엇을 제공하는가?입니다.

에이전트 오케스트레이션을 위한 실제 소프트웨어를 작성할 수 있는 능력을 제공합니다. 이제 자체 에이전트 오케스트레이션을 처음부터 끝까지 엔지니어링할 수 있습니다.

팩토리를 엔지니어링할 수 있습니다.

예를 들어, Github을 루프에서 모니터링하는 프로그램과 검토를 위해 QA 에이전트와 함께 구현 에이전트를 실행하는 별도의 프로그램을 만들 수 있습니다. 둘 다 실제 환경에서 접할 수 있는 개별적으로 유용한 패턴입니다. 그런 다음 이를 결합하여 PR의 댓글을 수신하고, 해당 댓글을 처리하기 위해 구현자를 생성한 다음, 수정 사항이 유효한지 확인하기 위해 QA 에이전트를 생성하는 시스템을 만들 수 있습니다.

그런 다음 이 프로그램을 작업 큐에 연결하여 코드베이스에서 작업을 위임 및 모니터링하고 PR 댓글에 자동으로 응답하도록 할 수 있습니다.

그리고 이 모든 작업을 빠른 오픈 웨이트 모델을 사용하여 수행할 수 있습니다. 코드일 뿐이므로 처음 작성된 후에는 오케스트레이션을 생각하기 위해 강력한 LLM이 필요하지 않습니다.

이제 재미있는 부분입니다. 대규모 출력 증가를 위해 사용해 온 프로그램 중 일부를 공유하겠습니다.

심층 코드베이스 연구

이 프로그램은 작업 범위를 정하는 데 도움이 됩니다. 모노레포의 상태에 대한 심층 연구를 수행하고 구현자가 참조할 연구 패킷을 준비합니다. 우리는 항상 이 프로그램을 사용합니다. 비용이 많이 들 것 같지만 실제로는 그렇지 않습니다. Slate에서 DeepSeek V4 Flash로 이 프로그램을 실행할 수 있으며 연구 프로세스는 철저하지만 매우 저렴합니다.

akira - inline image

목표-검토-PR

이것은 연구가 완료되면 작업을 구현하는 데 사용하는 것입니다. 운 좋게도 연구가 목표 프로그램에 도달할 때쯤이면 대부분의 작업 모호성이 해결되어 작업 실행 속도가 더욱 빨라집니다. 경량 OSS 모델로 연구를 사전 로드하면 중요한 작업, 즉 실제로 좋은 코드를 작성하고 시스템 상태를 확인하는 데 Opus와 같은 고가의 모델을 쉽게 사용할 수 있습니다. 프로그램을 수정하여 GPT 5.5를 사용하여 Opus 4.8의 작업을 적대적으로 검토할 수도 있습니다.

akira - inline image

프로그램으로서의 자동 연구

Autoresearch[[9]](http://localhost:5173/blog/onyx#ref-9)는 원래 전적으로 LLM 기반이었습니다. program.md 프롬프트에 에이전트를 지정하면 무엇을 시도하고 어떻게 진행할지 결정합니다.

놀랍지 않게도, Autoresearch는 실제로 프로그램일 뿐입니다.

에이전트 프로그램을 사용하면 이를 반전시키고 제어 흐름을 런타임에 배치할 수 있습니다. 프로그램이 제어 흐름을 소유하는 동안 에이전트는 부수 효과 작업(코드 편집, git 실행, 원격 GPU에 SSH, 훈련)을 수행합니다. autoresearch 프로그램의 경우 유지/되돌리기 결정은 결정론적 코드입니다:

typescript
1kept = status === "ok" && valBpb != null && valBpb < best

우리의 경우 프로그램은 설정 에이전트를 실행하여 새 repo를 준비하고 원격 A100에 연결할 수 있는지 확인합니다. 설정에 실패하면 입력된 값을 기반으로 깔끔하게 종료하고 조기에 반환합니다. 그렇지 않으면 실험 루프에 진입합니다.

각 실험은 새 에이전트를 얻습니다. 에이전트에는 현재 최상의 구성과 이전 아이디어 및 결과의 기록이 제공되므로 반복하지 않고 유지된 내용을 기반으로 구축할 수 있습니다. 변경 사항을 제안하고, train.py를 편집하고, 커밋하고, 원격 머신에 rsync하고, 훈련하고, 결과를 분류합니다.

에이전트와 프로그램은 상태를 공유합니다. 에이전트는 상태에 데이터를 쓰고 프로그램은 제어 흐름을 위해 상태를 평가합니다. 결과에 따라 기록 에이전트가 results.tsv를 업데이트하고, 프로그램이 실험을 폐기하기로 결정한 경우 선택적으로 실행을 재설정합니다. 이렇게 하면 git HEAD가 항상 실험 트리의 현재 최상의 분기를 가리키게 됩니다.

주목할 가치가 있는 두 가지 핵심 차이점이 있습니다: 1) 이것은 프로그램에서 실행되므로 실험당 새 에이전트를 생성할 수 있고 2) 라이브 프로그램 상태에 따라 에이전트가 수행해야 할 작업을 결정할 수 있습니다.

akira - inline image
akira - inline image

그리고 코드로 보면 다음과 같습니다:

typescript
1// ---------- 프로그램 ----------
2
3program(async (ctx) => {
4 const c = cfg(ctx.input)
5 const total = ctx.input?.maxExperiments ?? 20
6
7 const setup = await run("ar-setup", {
8 prompt: setupPrompt(c),
9 type: "general",
10 maxSteps: 40,
11 output: SetupResult,
12 })
13 if (!setup.ready) {
14 return { aborted: true, reason: `setup failed: ${setup.note}`, setup }
15 }
16
17 let best = c.baselineValBpb
18 let bestCommit = setup.baselineCommit
19 const history = []
20
21 for (let i = 1; i <= total; i++) {
22 let exp
23 try {
24 exp = await run(`ar-exp-${i}`, {
25 prompt: experimentPrompt(c, i, total, best, historyText(history)),
26 type: "general",
27 maxSteps: 80,
28 output: ExperimentResult,
29 })
30 } catch (err) {
31 // 에이전트 오류/차단 — 충돌로 처리, repo를 최상으로 복원, 계속.
32 exp = {
33 description: `experiment ${i} agent error`,
34 commit: "error",
35 status: "crash",
36 valBpb: null,
37 peakVramMb: null,
38 numSteps: null,
39 exitCode: -1,
40 retries: 0,
41 note: String(err?.message ?? err).slice(0, 200),
42 }
43 }
44
45 const kept = exp.status === "ok" && exp.valBpb != null && exp.valBpb < best
46
47 await run(`ar-record-${i}`, {
48 prompt: recordPrompt(c, exp, kept, bestCommit),
49 type: "general",
50 maxSteps: 20,
51 output: RecordResult,
52 })
53
54 if (kept) {
55 best = exp.valBpb
56 bestCommit = exp.commit
57 }
58
59 history.push({
60 idx: i,
61 description: exp.description,
62 status: exp.status,
63 valBpb: exp.valBpb,
64 kept,
65 commit: exp.commit,
66 retries: exp.retries,
67 })
68
69 await checkpoint({
70 name: `experiment-${i}`,
71 message: `exp ${i}/${total}: ${exp.status}${kept ? " KEPT" : ""} val_bpb=${exp.valBpb ?? "n/a"} (best=${best})`,
72 data: { i, total, status: exp.status, valBpb: exp.valBpb, kept, best, bestCommit },
73 })
74 }
75
76 const kepts = history.filter((h) => h.kept)
77 return {
78 baselineValBpb: c.baselineValBpb,
79 bestValBpb: best,
80 bestCommit,
81 improvement: c.baselineValBpb - best,
82 experimentsRun: history.length,
83 kept: kepts.length,
84 crashes: history.filter((h) => h.status === "crash").length,
85 infraFails: history.filter((h) => h.status === "infra_fail").length,
86 localRepo: c.localRepo,
87 branch: c.branch,
88 history,
89 }
90})

향후 작업

아직 정의하지 않은 유일한 VM 요구 사항은 프로그램의 내구성 모델입니다. 프로그램을 재개하고 수명 주기를 처리하는 올바른 모델이 무엇인지, 그리고 런타임에 어떤 수준의 제어가 노출되어야 하는지는 불분명합니다.

그 외에도 다양한 워크로드와 작업 형태를 지원하기 위해 추가할 흥미로운 기능이 많이 있습니다. 그래야 에이전트를 더 잘 오케스트레이션하는 실제 소프트웨어를 작성할 수 있습니다. 많은 패턴이 사람들이 프로그램을 자신만의 창의적인 방식으로 사용함에 따라 자연스럽게 나타날 것이라고 확신합니다.

여러분이 무엇을 만들지 정말 기대됩니다.

  • RL 팀

참고 문헌

  1. Yao et al., "ReAct: Synergizing Reasoning and Acting in Language Models," 2022
  2. Geoffrey Huntley, "The Ralph Loop"
  3. Geoffrey Huntley, "everything is a ralph loop," January 2026
  4. Zhang, Kraska, Khattab, "Recursive Language Models," December 2025
  5. Wang et al., "Executable Code Actions Elicit Better LLM Agents," ICML 2024
  6. OpenAI, "Introducing Deep Research," February 2025
  7. Cursor, "Scaling Agents," January 2026
  8. OpenAI, "Using Goals in Codex"
  9. Andrej Karpathy, "autoresearch"
  10. Anthropic, "Introducing Dynamic Workflows in Claude Code"
  11. OpenAI, "Learning to Reason with LLMs," September 2024
  12. Anthropic, "A harness for every task: dynamic workflows in Claude Code"
  13. Random Labs, "Skill Chaining"
YouMind에서 다시 만들기

Turn one viral article into a full content workflow

Collect the source, decode the pattern, create assets, draft the story, and distribute from one AI workspace.

Explore YouMind
크리에이터를 위해

당신의 Markdown을 깔끔한 𝕏 글로

직접 쓴 장문을 올릴 때 이미지, 표, 코드 블록을 𝕏에 맞게 정리하는 일은 번거롭습니다. YouMind는 전체 Markdown 초안을 깔끔하고 바로 게시할 수 있는 𝕏 글로 바꿔 줍니다.

Markdown → 𝕏 사용해 보기

분석할 패턴 더 보기

최근 바이럴 아티클

더 많은 바이럴 아티클 보기