실행 플랫폼
0f085aa 기준입니다.
ax는 에이전트 작업을 YAML로 선언하면, 필요한 작업 공간을 준비하고 격리된 환경에서 실행·중단·재개하도록 조정하는 플랫폼입니다. AX의 핵심은 모델에게 무엇을 말할지가 아니라, 에이전트가 어디서 어떤 자원으로 일하게 할지를 관리하는 데 있습니다.
예를 들어 코드 검사 에이전트 100개를 실행한다고 해 봅시다. 각 에이전트에는 저장소, 실행 명령, 메모리 한도, API 인증 정보가 필요합니다. 문제가 생기면 내부를 들여다봐야 하고, 오래 쉬는 작업은 멈췄다가 다시 이어가고 싶습니다. AX는 이 운영 문제를 세 가지 자원으로 나눕니다.
| 자원 | 쉬운 뜻 | 실제 선언 내용 |
|---|---|---|
| Task | 한 에이전트의 작업 주문서 | 이미지, 명령, 환경변수, CPU·메모리 한도, Workspace 참조, 디버그 허용 여부 |
| Workspace | 작업실 준비 목록 | Git 저장소, 파일 내용, MCP 설정, 스킬 설정 |
| Model | 모델 접속 설정표 | 제공자, 모델 식별자, 생성 매개변수, Kubernetes Secret 참조 |
이 구조의 원본은 pkg/apis/v1alpha1/ax.proto입니다. 이름에 붙은 v1alpha1은 초기 단계 API임을 드러냅니다.
선언할 수 있는 필드와 실제 실행 경로에서 소비하는 필드는 구분해야 합니다. 특히 MCP·스킬의 구현 범위는 뒤에서 따로 확인합니다.
Task는 연구 지시서, Workspace는 책상에 놓을 자료와 도구 목록, Model은 자문할 전문가의 연락처입니다. Agent Substrate는 방과 전기·출입 통제를 제공하는 건물에 가깝습니다.
연구원이 똑똑해도 방을 공유하다 서로의 파일을 지우거나, 자리를 비운 채 비용을 계속 쓰면 운영이 어렵습니다. AX를 읽는 이유는 연구원의 지능을 높이는 법보다, 여러 연구원이 함께 일할 기반을 만드는 법을 배우기 위해서입니다.
실행 파일은 세 개입니다. cmd/ax가 사용자의 CLI, cmd/ax-server가 제어 서버,
cmd/ax-task-runner가 각 샌드박스 안의 시작점입니다. 서버와 실행기를 분리한 덕분에 사용자는 실행기 이미지를 바꿀 수 있습니다.
입력 검증, RPC, 상태 저장, 분산 잠금, 격리 실행, 준비 상태 확인을 하나의 흐름으로 연결할 수 있습니다. 초보자에게는 거대한 저장소보다 경계가 분명한 작은 분산 시스템을 읽는 연습이 됩니다.
2026-10-23 GitHub Trending Go Monthly 1위, 스타 14,049개, 포크 732개입니다. 이 수치만으로 인기의 원인을 입증할 수는 없습니다. 다만 Google의 공개 에이전트 실행 기반이라는 위치와 아래 설계 선택들은 개발자가 이 저장소에 주목할 이유로 해석할 수 있습니다.
README는 에이전트가 상태를 쌓고, 모델 API와 도구 서버에 접근하며, 반복 실행으로 비용을 소모할 수 있다고 설명합니다. AX는 이 문제에 격리, 자원 한도, 작업 공간 준비, 명시적인 중단·재개라는 관리 단위를 제공합니다. 에이전트 로직이 바뀌어도 실행 환경을 관리하는 규칙은 재사용할 수 있다는 점이 이 설계의 학습 가치입니다.
apiVersion·kind·metadata·spec 형태와
apply·get·watch 명령은 Kubernetes와 닮았습니다.
하지만 Task를 Kubernetes CRD로 등록하는 구조는 아닙니다. DESIGN.md는 수많은 짧은 작업이
Kubernetes의 etcd에 주는 부담을 피하려고 Redis를 선택했다고 설명합니다.
여기서 배울 것은 특정 DB가 언제나 더 빠르다는 결론이 아닙니다. 인프라 배포를 관리하는 데이터와 대량의 작업 상태 데이터를 같은 저장 경로로 보내야 하는지 묻는 제어 경계의 설계입니다.
| 선택지 | 잘 맞는 문제 | AX를 선택할 이유와 비용 |
|---|---|---|
| 직접 만든 Go 실행기 | 소수의 명령을 시작하고 결과 수집 | AX에는 자원 명세·상태 API·원격 점검 경로가 이미 있다. 대신 Redis와 Substrate 운영이 추가된다. |
| Kubernetes Job | 완료를 목표로 하는 배치 작업 | AX는 작업 공간과 중단·재개를 자체 API로 다룬다. 단, Job 생태계 전체를 대체한다는 뜻은 아니다. |
| 에이전트 로직 프레임워크 | 모델 호출·도구 선택·대화 흐름 구성 | AX의 중심은 격리 실행과 수명주기다. 실제 에이전트 로직은 작업 명령이나 사용자 이미지 안에 둔다. |
| Agent Substrate 직접 사용 | Actor와 실행 기반을 직접 제어 | AX는 그 위에 Task·Workspace·Model과 CLI를 올린다. 대신 Substrate API의 모든 기능을 노출하지는 않는다. |
비교의 기준은 역할과 구조입니다. AX가 다른 도구보다 빠르거나 저렴하다는 실측 비교는 아닙니다. 특히 에이전트의 사고 흐름과 실행 인프라는 함께 조합할 수 있는 서로 다른 층입니다.
자동 생성 코드를 제외한 Go 구현은 7,237줄이고, Go 테스트는 6,751줄입니다.
internal/lock/lock.go에는 잠금 갱신 실패의 한계까지 주석으로 적혀 있습니다.
runner/runner.go에는 자식 프로세스 종료, 종료 신호 전파, 정리 시간 제한이 한 파일에 모여 있습니다.
README는 클러스터당 수십억 작업을 목표로 내세웁니다. 이 소스에서 확인되는 E2E 테스트는 기능과 수명주기를 검증하는 구성입니다. 처리량·동시 실행 수·장애 복구 시간을 입증하는 대규모 벤치마크 수치로 읽으면 안 됩니다.
자동 유휴 감지, 상태를 가진 작업의 분기, SPIFFE 기반 작업 신원, 원격 측정 확장은
docs/roadmap.md에 계획으로 적혀 있습니다. 이미 완성된 기능 목록과 구분하세요.
go.mod의 Go 지시문은 go 1.27.1입니다.
docs/development.md는 Go 1.27 이상이라고 안내하지만, 실제 빌드 기준을 볼 때는 더 구체적인 모듈 선언을 먼저 읽습니다.
| 의존성 | 실제 선언 버전 | 역할과 확인할 파일 |
|---|---|---|
| Agent Substrate | v0.5.0 | Actor·템플릿·중단·재개 API. internal/substrate/client.go |
| Agent env | v0.0.11-0.20260912052224-4468a200b170 | 게스트의 프로세스·파일 서비스. internal/guest/client.go |
| go-redis/v9 | v9.22.0 | 상태 저장, 정렬 집합, Pub/Sub, Lua 잠금 |
| gRPC | v1.83.2 | CLI↔제어 서버, 서버↔Substrate 통신 |
| Protobuf | v1.36.12 | 공유 자원 모델, RPC 타입, JSON 직렬화 |
| yaml.v3 | v3.0.1 | 여러 YAML 문서 읽기와 YAML 노드 변환 |
| backoff/v5 | v5.0.3 | 일시적 Substrate 오류의 지수 백오프 |
날짜와 커밋 식별자가 붙은 긴 Agent env 버전은 의사 버전입니다. 날짜·시간과 커밋 식별자를 포함해 특정 소스를 가리킵니다. 보기 편한 릴리스 이름이 없어도 의존성을 재현 가능한 위치에 고정하는 Go 모듈의 방법입니다.
// go.mod의 직접 의존성 중 일부
go 1.27.1
require (
github.com/agent-substrate/substrate v0.5.0
github.com/redis/go-redis/v9 v9.22.0
google.golang.org/grpc v1.83.2
google.golang.org/protobuf v1.36.12
gopkg.in/yaml.v3 v3.0.1
)
net/http, flag, log/slog, os/exec 같은 Go 표준 패키지도 중요합니다.
별도의 웹 프레임워크나 Cobra 없이 서버와 CLI를 구성합니다. cmd/ax/main.go의
parseGlobalArgs()·runApply()를 읽으면 명령 처리의 기본을 직접 볼 수 있습니다.
이 커밋에는 package.json과 React·Vue 같은 웹 앱이 없습니다.
사용자 접점은 ax CLI이며, cmd/ax/main.go 한 파일이 1,219줄입니다.
입력은 YAML, 요청은 gRPC, 출력은 표·상세 텍스트·YAML·상태 스트림입니다.
| 접점 | 구현 | 초보자가 볼 부분 |
|---|---|---|
| 선언 입력 | runApply()·applyDocument() | YAML의 kind에 따라 다른 RPC로 분기 |
| 실시간 상태 | runWatch()·WatchTask() | 요청 한 번으로 응답을 계속 받는 서버 스트리밍 |
| 클러스터 연결 | internal/tunnel/ | kube context를 읽고 제어 서버 연결 경로 준비 |
| 원격 작업 점검 | runSSH()·internal/guest/ | SSH와 닮은 명령 뒤의 게스트 gRPC 서비스 |
pkg/apis/v1alpha1/ax.proto가 계약서이고,
pkg/apis/v1alpha1/ax.pb.go·pkg/apis/v1alpha1/ax_grpc.pb.go가 그 계약서에서 만들어진 Go 코드입니다.Dockerfile.task-runner는 python:3.12-slim을 기반으로 Git·curl·bash 등을 설치하고,
Go 실행기와 cmd/ax-task-runner/antigravity_bootstrap.py를 넣습니다. Python은 129줄의 초기화 연결 코드에 사용됩니다.
google-antigravity는 pip install에 버전 고정 없이 선언되어 있습니다.
Python 패키지용 pyproject.toml이나 잠금 파일은 없습니다. 따라서 Go 모듈 버전이 고정되었다고 해서
실행 이미지 전체의 의존성이 같은 수준으로 고정된 것은 아닙니다.
| 영역 | 실제 설정 | 해석 |
|---|---|---|
| 제어 서버·DB | deploy/ax-server.yaml, redis:7-alpine | Kubernetes Deployment·Service로 배치. Redis의 세부 패치 버전은 고정하지 않음 |
| Go 이미지 | .ko.yaml, CGO_ENABLED=0, -trimpath | ko 사용. 서버 기본 이미지는 cgr.dev/chainguard/static:latest |
| 작업 이미지 | Makefile의 build-task-runner | Linux amd64로 교차 컴파일한 뒤 Dockerfile로 Python 포함 이미지 생성 |
| 단위 테스트 CI | .github/workflows/go.yml | 모듈 정리 차이 검사, 바이너리 세 개 빌드, go test -v ./... |
| 통합 테스트 CI | .github/workflows/e2e.yml | Kind·Substrate·gVisor 환경. PR은 go.mod 버전, 야간 작업은 Substrate main을 대상으로 구성 |
기술 스택을 읽을 때는 라이브러리 이름뿐 아니라 버전이 고정되는 경계와 실제 배포 경로까지 함께 확인해야 합니다.
.ko.yaml의 실행기 기반은 alpine/git:latest로, Python을 포함하는 Dockerfile 경로와 다릅니다.
목표 기반 초기화에 필요한 도구가 어느 이미지에 들어가는지 구분해서 읽으세요.
AX가 직접 가상화 엔진을 구현하는 것은 아닙니다. internal/substrate/client.go의
BuildActorTemplate()는 샌드박스 클래스를 GVisor, 설정 이름을 gvisor-default로 지정합니다.
격리의 실제 동작과 지원 환경은 Substrate 쪽 구현·설정에도 달려 있습니다.
pkg/apis/v1alpha1/ax.proto에 자원과 RPC를 함께 정의합니다.
YAML은 pkg/apis/v1alpha1/types.go의 unmarshalYAMLWith()에서 일반 데이터로 읽은 뒤 JSON으로 변환하고,
마지막에 protojson으로 해석합니다. YAML과 RPC의 필드 이름·타입이 같은 계약을 따르게 하는 설계입니다.
// pkg/apis/v1alpha1/types.go의 옵션
protoUnmarshal = protojson.UnmarshalOptions{}
protoUnmarshalLenient = protojson.UnmarshalOptions{
DiscardUnknown: true,
}
// 사용자 YAML: unmarshalYAML() → 엄격하게 해석
// 실행기 입력: UnmarshalYAMLLenient() → 모르는 필드는 무시
사용자가 memory를 memroy로 쓰면 빠르게 오류를 알려야 합니다.
반대로 새 서버가 추가한 필드를 오래된 실행기가 모른다는 이유로 작업 전체가 멈추면 업데이트가 어려워집니다.
cmd/ax-task-runner/main.go의 loadSpec()가 관대한 해석을 사용하는 이유입니다.
주문을 받는 창구에서는 메뉴 이름의 오타를 잡습니다. 주방에 전달된 주문서에 새 관리 항목이 추가됐다고 해서 예전 조리 기계가 모든 주문을 거절하게 하지는 않습니다. 같은 문서도 어느 경계를 지나는지에 따라 검증 목적이 달라집니다.
커밋 0f085aa는 바로 이 실행기 호환성을 보완합니다.
cmd/ax-task-runner/main_test.go의 TestLoadSpec_IgnoresUnknownFields()와
TestLoadSpec_WorkspaceIgnoresUnknownFields()가 그 의도를 보여 줍니다.
다만 모르는 필드를 무시한다는 것은 그 필드의 기능까지 구현한다는 뜻은 아닙니다.
internal/store/store.go의 Store는 저장·조회·삭제·감시 계약입니다.
실제 Redis 구현은 internal/store/redis/store.go, 테스트에 쓰기 쉬운 메모리 구현은
internal/store/memory/store.go에 있습니다. API 서버는 구체적인 Redis 명령 대신 이 인터페이스를 호출합니다.
internal/server/server.go의 CreateTask()는 검증과 잠금 획득 후 Task를 저장하고,
같은 요청 안에서 Reconcile()를 호출합니다. 이름이 controller라고 해서 별도 백그라운드 작업 큐가
모든 상태를 계속 재조정한다고 가정하면 안 됩니다.
README와 다른 중요한 지점: 빠른 시작의 출력은 apply 뒤 Running을 보여 주지만,
이 커밋의 생성 경로는 Suspended입니다. applyDocument()에도 자동 재개 호출이 없습니다.
TestE2E_TaskLifecycleAndSubstrateSync() 역시 생성 후 별도의 ax resume task를 호출합니다.
Task의 명세는 생성 후 불변입니다. 이미 존재하는 Task를 다시 적용하면 보통 거절하며,
Pending 상태에서 동일 명세로 생성 요청을 다시 보내는 복구 경로는 허용합니다.
Workspace·Model의 생성 또는 갱신과 Task의 생성을 같은 동작으로 이해하지 마세요.
taskTemplateName()는 이미지와 환경변수를 정렬된 순서로 해시해 템플릿 이름에 넣습니다.
실행용 Task 복사본에서 Status를 제거하므로, 단순한 상태 변화가 새로운 시작 템플릿을 만들지 않습니다.
설정과 관측 상태를 분리하는 구체적인 예입니다.
SaveTask()는 TxPipeline()으로 자원 JSON 저장, 두 개의 정렬 집합 갱신,
Pub/Sub 발행을 묶습니다. 기본 키는 ax:task:default:hello처럼 만들어집니다.
서버 시작 시 저장소 옵션은 비어 있어 Task 레코드 TTL은 기본적으로 설정되지 않습니다.
// internal/store/redis/store.go, SaveTask()의 핵심
pipe := s.client.TxPipeline()
pipe.Set(ctx, s.taskKey(atespace, name), data, s.opts.TTL)
pipe.ZAdd(ctx, s.taskIndexKey(), redis.Z{Score: score, Member: member})
pipe.ZAdd(ctx, s.taskAtespaceIndexKey(atespace), redis.Z{Score: score, Member: name})
pipe.Publish(ctx, s.taskPubSubChannel(atespace, name), data)
_, err = pipe.Exec(ctx)
WatchTask()는 먼저 구독을 준비하고 현재 값을 INITIAL로 보낸 뒤,
이후 알림을 MODIFIED로 전달합니다. Redis Pub/Sub는 과거 알림을 재생하는 영속 로그가 아닙니다.
연결이 끊긴 동안의 모든 변화를 보장하는 이벤트 저장소로 해석하면 안 됩니다.
두 서버가 같은 Task를 동시에 중단·재개하면 상태가 꼬일 수 있습니다.
internal/lock/lock.go의 RedisLocker.Lock()는 자원의 종류·atespace·이름을 키로 만들어
같은 자원에 대한 변경을 직렬화합니다. 서로 다른 Task 전체를 하나의 전역 잠금으로 묶지는 않습니다.
| 장치 | 실제 값·구현 | 해결하는 문제 |
|---|---|---|
| 획득 | SetNX + 임의 토큰 | 키가 비어 있을 때 한 요청만 소유 |
| 임대 시간 | 기본 30초 | 소유 프로세스가 죽었을 때 잠금이 영구히 남지 않게 함 |
| 갱신 | renew(), TTL/3마다 | 정상적인 긴 작업이 30초를 넘겨도 잠금 유지 시도 |
| 해제·알림 | Lua에서 토큰 확인 후 삭제·발행 | 다른 소유자의 잠금을 지우지 않고 대기자 깨우기 |
| 알림 누락 대비 | 기본 1초 재시도 | Pub/Sub 알림 없이 만료된 키도 다시 획득 |
코드 주석은 펜싱 토큰이 없고 갱신이 최선 노력 방식임을 명시합니다. Redis 장애가 TTL보다 길면 기존 요청이 계속 실행되는 중에 다른 요청이 잠금을 얻을 수 있습니다. “분산 잠금이 있으니 모든 중복 실행이 불가능하다”는 보장은 아닙니다.
runner/runner.go의 Run()은 메타데이터 서버를 시작하고 작업 공간을 준비한 뒤,
사용자 명령을 별도 프로세스 그룹으로 실행합니다. 명령 종료를 감지해도 실행기는 취소 신호를 기다리며 살아 있습니다.
사용자 명령의 종료, 샌드박스의 생존, 작업 공간의 준비 상태는 서로 다른 사실로 관리됩니다.
// runner/runner.go, Run()에서 발췌
exited := make(chan error, 1)
go func() { exited <- cmd.Wait() }()
select {
case err := <-exited:
reportExit(cfg, cmd, err)
// 명령이 끝나도 점검할 수 있도록 실행기를 유지한다.
<-ctx.Done()
case <-ctx.Done():
reportExit(cfg, cmd, stopCommand(cmd, exited))
}
stopCommand()는 프로세스 그룹에 SIGTERM을 보내고 10초를 기다린 뒤,
끝나지 않으면 SIGKILL을 보냅니다. 자식이 만든 하위 프로세스까지 함께 정리하려고
Setpgid: true와 음수 PID를 사용합니다.
OnCommandExit 훅은 있지만 기본 실행기의 main()은 이를 서버 상태 갱신에 연결하지 않습니다.
그러므로 명령이 정상 종료했다고 Task가 자동으로 Completed가 된다고 가정하면 안 됩니다.
TestRun_KeepsServingAfterCommandExits()는 실행기를 계속 유지하려는 의도를 보여 줍니다.
| 신호·접점 | 구현상 의미 | 보장하지 않는 것 |
|---|---|---|
| 서버 /healthz | 제어 서버 HTTP 핸들러가 응답 | Redis·Substrate까지 정상이라는 종합 확인 |
| 실행기 /readyz | 내부 workspaceReady 값이 참 | 모든 Git·초기화 작업의 실제 성공 |
| Running | Actor 재개 후의 단계 요약 | 작업 공간 준비 완료, 명령의 성공 종료 |
| Ready 조건 | Actor 재개 성공과 WorkspaceReady 조건을 조합 | 매번 작업 공간을 새로 확인하거나 이후 상태를 계속 감시한다는 보장 |
특히 SetupWorkspace()는 Git 실패를 기록하고 초기화 마커 생성을 생략해도 nil 오류를 반환합니다.
Run()은 오류가 없으면 준비 상태를 참으로 만들고 SetupResult의 세부 결과를 확인하지 않습니다.
초기화 에이전트의 실패·건너뛰기도 치명적 오류가 아닙니다. 조정기는 한 번 참이 된 WorkspaceReady 조건을 중단·재개 후에도 재사용합니다.
준비 상태와 실제 준비 품질 사이의 차이를 읽는 연습이 됩니다.
메타데이터는 127.0.0.1:8099, 외부 준비 확인과 게스트 서비스는 기본 :80으로 분리합니다.
internal/metadata/server.go의 NewServer()는 spec.debug가 참일 때만
프로세스 실행·파일 접근 게스트 서비스를 등록합니다. 사용 가능한 기능에 따라 노출 면적을 줄이는 설계입니다.
cmd/는 각 실행 파일의 입구입니다. 실제 동작 대부분은 internal/과 runner/에 있습니다.
Go의 internal 경로는 바깥 프로젝트의 임의 가져오기를 제한합니다. 반면 공개 runner 패키지는
사용자 이미지가 runner.Run()을 호출하도록 제공하는 확장 지점입니다.
| 읽기 순서 | 파일·함수 | 답할 질문 |
|---|---|---|
| 1. 계약 | pkg/apis/v1alpha1/ax.proto, ValidateTask() | 사용자가 무엇을 선언할 수 있는가? |
| 2. 접수 | cmd/ax/main.go, applyDocument() | YAML은 어떤 요청이 되는가? |
| 3. 조정 | Server.CreateTask(), TaskReconciler.Reconcile() | 언제 저장하고 언제 실제 실행 기반을 부르는가? |
| 4. 실행 | runner.Run(), SetupWorkspace() | 샌드박스 안에서 어떤 순서로 준비하는가? |
| 5. 의도 | *_test.go, test/e2e/ | 어떤 동작을 유지하려고 테스트하는가? |
커밋 0f085aa의 확장자별 물리적 줄 수입니다. 빈 줄·주석을 포함하며 Git 내부 파일은 제외합니다.
자동 생성 코드를 분리해야 실제로 읽어야 할 구현 규모가 보입니다.
| 언어·분류 | 파일 수 | 줄 수 |
|---|---|---|
| Go 구현 | 20 | 7,237 |
| Go 테스트 | 19 | 6,751 |
| Go 생성 코드 | 2 | 3,458 |
| Go 합계 | 41 | 17,446 |
| Python | 1 | 129 |
| Protocol Buffers | 1 | 304 |
| Shell | 5 | 560 |
| YAML / YML | 12 | 723 |
| Markdown | 13 | 995 |
생성 코드를 뺀 Go 13,988줄 중 테스트는 약 48.3%입니다. 이는 줄 수의 비중이며 코드 커버리지 수치가 아닙니다. 초보자는 생성 코드 3,458줄부터 읽기보다, 계약을 정의한 proto와 테스트가 설명하는 동작부터 읽는 편이 좋습니다.
store.Store, lock.Locker, server.Reconciler는 구현 교체 경계입니다.
서버가 Redis 클라이언트를 직접 받는 대신 저장소 계약을 받으면, 빠른 테스트에는 메모리 구현을 넣을 수 있습니다.
server.NewServer()가 Options로 잠금과 조정기를 주입받는 부분을 함께 읽으세요.
실습 아이디어: CreateTask()가 실제로 사용하는 인터페이스 메서드만 표시한 의존성 표를 그려 보세요.
“인터페이스가 있으면 좋다”를 넘어, 외부 서비스를 어느 지점에서 대체할 수 있는지 설명하는 것이 목표입니다.
pkg/apis/v1alpha1/types.go의 YAML→JSON→Protobuf 변환은 필드 명명 규칙을 한곳에 모읍니다.
pkg/apis/v1alpha1/ax.proto의 worker_ip에는 json_name = "workerIP"를 명시해 기존 표기를 유지합니다.
내부 언어의 관례와 외부 사용자에게 약속한 이름을 구분하는 예입니다.
실습 아이디어: 정상 YAML, 오타 필드가 있는 YAML, 새 필드가 추가된 YAML을 종이에 작성하고,
CLI와 실행기가 각각 수락할지 예상해 보세요. pkg/apis/v1alpha1/types_test.go와 실행기 테스트에서 근거를 찾습니다.
Task의 현재 값은 문자열 키, 목록은 정렬 집합, 변화 알림은 Pub/Sub입니다.
같은 Redis를 사용하지만 자료구조마다 보장하는 동작은 다릅니다. GetTask()가 읽는 경로와
WatchTask()가 듣는 경로를 별도로 추적해 보세요.
실습 아이디어: watcher가 연결되지 않은 동안 상태가 두 번 변했다고 가정하세요. 다시 연결했을 때 현재 값으로 알 수 있는 사실과, 알림 기록 없이는 알 수 없는 사실을 두 열로 적어 봅니다.
context.Context는 시간 제한과 취소를 하위 동작에 전달합니다.
Run()의 버퍼가 1인 종료 채널은 프로세스 종료 결과 하나를 전달하고,
잠금의 sync.Once는 해제 동작이 중복 호출돼도 한 번만 수행되게 합니다.
실습 아이디어: 명령이 먼저 끝나는 경우, 취소가 먼저 오는 경우, SIGTERM을 무시하는 경우를 각각 시간축으로 그리세요. 어느 goroutine이 무엇을 기다리고 누가 채널에 쓰는지 표시하면 됩니다.
internal/substrate/client.go의 retryOnCodes()는 최대 10회,
초기 간격 200ms, 최대 간격 2초의 지수 백오프를 사용합니다. Git 준비는 최대 5회, 실패 사이 2초 간격으로 별도 구현합니다.
실패가 같아 보여도 상대 서비스와 작업 성격에 따라 재시도 정책이 다릅니다.
실습 아이디어: 네트워크 일시 단절, 잘못된 메모리 단위, 없는 이미지 이름을 분류해 보세요.
IsTransientError()와 ValidateResources() 중 어느 경계가 먼저 책임져야 할지 설명합니다.
internal/metadata/server.go는 메타데이터와 원격 실행 서비스를 다른 리스너로 나누고,
디버그 설정에 따라 게스트 서비스를 켭니다. cmd/ax-task-runner/antigravity_bootstrap.py는
파일 도구에 작업 디렉터리 정책을 적용하면서 명령 실행도 허용합니다. 이 정책과 OS 수준의 격리는 서로 다른 층입니다.
실습 아이디어: “Task가 Running이다”, “/readyz가 200이다”, “Git 준비가 성공했다”, “사용자 명령이 성공했다” 사이에 성립하는 관계만 화살표로 연결하세요. 소스에 없는 보장을 추가하지 않는 것이 핵심입니다.
함수마다 “성공하면 무엇이 저장되는가”, “실패하면 무엇이 남는가”, “다시 요청해도 안전한가”를 적어 보세요. API·저장소·실행기 중 어느 층이 그 질문에 답하는지 구분할 수 있으면, 다른 서버 프로젝트도 훨씬 읽기 쉬워집니다.
저장소에는 개발용 PC의 공식 최소 RAM·디스크 용량이나, 작업 수에 따른 용량 산정표가 없습니다. 필요 자원은 AX 제어 서버, Redis, Substrate 작업자, 실제 에이전트 작업의 몫을 나누어 계산해야 합니다. 아래 수치는 성능 측정값이 아니라 배포 예시의 설정값입니다.
| 대상 | 확인되는 설정·요건 | 해석 |
|---|---|---|
| 소스 학습 | 텍스트 편집기와 파일 열람 도구 | 클러스터·API 키·GPU 없이도 구조 학습 가능 |
| Go 빌드 | go.mod의 Go 1.27.1 | CI는 go.mod를 기준으로 도구 버전 선택 |
| 제어 서버 | 요청 100m / 128Mi, 한도 1000m / 1Gi | deploy/ax-server.yaml, 복제본 1개 |
| Redis | 요청 100m / 128Mi, 한도 1000m / 1Gi | deploy/redis.yaml, 복제본 1개 |
| 실제 작업자 | Substrate와 gVisor 실행 환경 | 제어 서버·Redis의 자원 설정에 포함되지 않음 |
| 배포 도구 | Kubernetes, kubectl, ko, 이미지 레지스트리 | 작업 이미지에는 Docker 또는 Podman 경로도 사용 |
| E2E 환경 | Kind, gVisor WorkerPool, 스냅샷 저장소 | hack/setup-e2e-kind.sh의 기본 작업자 복제본은 4개 |
100m은 CPU 0.1코어, 1000m은 1코어입니다.
두 제어 구성요소의 요청 합계는 CPU 0.2코어·메모리 256Mi이지만,
Kubernetes 자체와 Substrate·실제 작업은 빠져 있으므로 노트북 전체의 최소 사양으로 사용할 수 없습니다.
pkg/apis/v1alpha1/resources.go의 ValidateResources()는
spec.resources.requests의 CPU나 메모리 값이 비어 있지 않으면 거절합니다. 스키마에 필드가 있어도 자원 크기 지정에 지원하는 설정은 limits뿐입니다.
CPU·메모리는 양수여야 하고, CPU는 1,000코어 미만이어야 합니다. 생략하면 작업자 기본 설정을 따릅니다.
# Task.spec 안의 예시: Pod의 requests와 혼동하지 않는다.
resources:
limits:
cpu: "500m"
memory: "512Mi"
이 값은 internal/substrate/client.go의 ResourceLimits()를 통해 ActorTemplate로 전달됩니다.
제한을 요청한 작업에 템플릿 생성이 실패했을 때, 다른 기본 템플릿으로 조용히 대체하지 않도록 제어 코드가 오류를 반환합니다.
기본 배포에는 GPU 자원 요청이 없고, 모델 클라이언트는 외부 생성 API를 호출하는 구조입니다. 따라서 AX 제어 기능 자체를 배우기 위해 GPU가 필요하다는 근거는 없습니다. 다만 사용자가 실행할 이미지가 로컬 추론이나 무거운 빌드를 포함한다면 그 작업의 요구사항을 별도로 계산해야 합니다.
모델 기반 초기화에는 이미지 안의 초기화 스크립트와 GEMINI_API_KEY가 필요합니다.
기본 제한 시간은 10분이며 AX_BOOTSTRAP_TIMEOUT으로 조정합니다.
Git 다운로드, 이미지 다운로드, 모델 API 호출, 스냅샷 저장소는 각각 네트워크 접근과 인증 구성이 필요할 수 있습니다.
Redis 예시에는 영속 볼륨 선언이 없고 복제본도 1개입니다. 제어 서버의 /healthz는
의존 서비스의 상태를 확인하지 않습니다. 상태 보존·백업·장애 감지·인증 경계를 별도로 설계해야 합니다.
AX_SNAPSHOTS_BUCKET의 기본값과 배포 예시에는 특정 환경의 저장 위치가 들어 있습니다.
자신의 저장소와 권한으로 바꿔야 합니다. 기본 템플릿의 스냅샷 선호도는 SNAPSHOT_FIDELITY_VOLUMES이므로,
중단·재개를 곧바로 모든 메모리 상태의 완전 복원이라고 단정하지 마세요.
앞의 두 과제는 파일 열람만으로 끝낼 수 있습니다. 실행 실험은 Substrate가 준비된 별도의 학습용 클러스터를 전제로 합니다. 각 과제는 명령 실행 자체보다 예상한 상태와 실제 구현이 일치하는지 설명하는 것을 완료 기준으로 삼습니다.
문제: Task.spec.workspace.path가 어디에서 검증되고,
어디에서 사용자 명령의 작업 디렉터리가 되는지 찾아보세요.
pkg/apis/v1alpha1/ax.proto에서 WorkspaceRef를 찾습니다.pkg/apis/v1alpha1/types.go에서 ValidateTask()와 WorkspacePath()를 읽습니다.runner.Run()의 cmd.Dir까지 연결합니다.완료 기준: 생략 시 /workspace, 상대 경로 선언 시 검증 오류,
절대 경로 선언 시 실행기의 작업 디렉터리로 사용된다는 세 경우를 설명합니다.
단, 해당 경로가 실제로 영속 저장되는지는 볼륨 마운트를 별도로 확인해야 합니다.
문제: spec.fieldFromTheFuture라는 알 수 없는 필드를 추가했다고 가정합니다.
같은 입력을 CLI와 실행기에 넣으면 어떤 차이가 날지 예상해 보세요.
힌트: UnmarshalYAML() 경로와 UnmarshalYAMLLenient() 경로,
cmd/ax-task-runner/main_test.go를 나란히 읽습니다.
완료 기준: “오타를 막는 사용자 경계”와 “서버·이미지 버전 차이를 견디는 내부 경계”를 설명하고, 필드 무시가 새로운 기능의 지원을 의미하지 않는 이유를 적습니다.
준비: 접근 가능한 AX·Substrate와 작업 이미지가 필요합니다. 서버의 --default-task-image 또는 AX_DEFAULT_TASK_IMAGE에 준비한 이미지를 지정합니다. 아래 선언은 모델 호출이나 목표 기반 초기화 없이 인라인 파일과 명령만 사용합니다. Workspace를 Task보다 먼저 선언합니다.
apiVersion: ax.io/v1alpha1
kind: Workspace
metadata:
name: study-ws
spec:
files:
- path: message.txt
content: "AX 학습용 파일"
---
apiVersion: ax.io/v1alpha1
kind: Task
metadata:
name: study-task
spec:
workspace:
name: study-ws
debug: true
resources:
limits:
cpu: "500m"
memory: "512Mi"
command: ["sh", "-c", "pwd; cat message.txt"]
# study.yaml로 저장한 뒤, 준비된 학습용 클러스터에서 관찰
ax apply -f study.yaml
ax get task study-task
# 생성 직후 예상 단계: Suspended
ax resume task study-task
ax describe task study-task
ax ssh study-task -- cat /workspace/message.txt
ax suspend task study-task
ax resume task study-task
ax delete task study-task
ax delete workspace study-ws
완료 기준: 생성 직후 Suspended, 재개 후 단계와 조건,
사용자 명령이 끝난 뒤에도 내부 파일 점검이 가능한 이유를 설명합니다.
ax watch는 상태 변화를 보는 명령이며 사용자 명령의 표준출력 로그를 대신하지 않습니다.
문제: Git 준비 실패 시 마커는 남지 않지만, 실행기가 준비 상태를 참으로 만드는 경로가 있습니다.
SetupResult와 Run() 사이의 계약을 어떻게 바꿀지 설계해 보세요.
TestSetupWorkspace_GitError()와 실행기 테스트를 확장할 검증 조건을 적습니다.완료 기준: 성공·일부 실패·재시작의 표를 만들고, 각각 반환값·마커·준비 응답·명령 실행 여부를 정합니다. 기존의 실패 허용 정책을 바꾸는 호환성 영향도 한 문단으로 설명합니다.
문제: 서버 A가 작업을 진행하는 동안 Redis 연결이 30초 넘게 끊기고, 서버 B가 새 임대를 얻었다고 가정합니다. 실제 장애를 일으키지 않고 두 서버와 Redis의 시간축을 그리세요.
읽을 코드: RedisLocker.Lock()·renew(),
releaseAndNotifyScript, Server.ResumeTask().
완료 기준: 토큰 비교가 막는 잘못된 잠금 해제와, 현재 구조가 막지 못하는 외부 작업 중첩을 구분합니다. 펜싱이나 임대 상실 시 취소를 도입하려면 어느 구성요소가 협력해야 하는지도 제안합니다.
Go 문법을 막 배웠다면 하루 30~60분의 읽기·메모를 기준으로 다음 순서를 활용할 수 있습니다. 일정은 학습용 제안이며, 각 주의 결과물을 설명할 수 있을 때 다음 단계로 넘어가세요.
| 주차 | 주제와 읽을 코드 | 남길 결과물 |
|---|---|---|
| 1주차 | Go 구조체·인터페이스·오류. Store, ValidateTask() | 세 자원의 필드 표와 입력 오류 사례 5개 |
| 2주차 | YAML·JSON·Protobuf·gRPC. pkg/apis/v1alpha1/types.go, applyDocument() | YAML부터 CreateTask까지의 변환 지도 |
| 3주차 | Redis 자료구조·트랜잭션·Pub/Sub. SaveTask(), WatchTask() | 현재 값·목록·알림의 키와 보장 비교표 |
| 4주차 | context·goroutine·프로세스 신호. Run(), stopCommand() | 정상 종료·취소·강제 종료의 시간축 |
| 5주차 | Kubernetes 배포·gVisor·Substrate. BuildActorTemplate(), deploy/ | 제어 서버와 실제 작업의 책임·자원 경계도 |
| 6주차 | 임대·멱등성·장애 복구. internal/lock/lock.go, internal/controller/reconciler.go, E2E 테스트 | 중복 요청·연결 단절·초기화 실패 설계 검토서 |
학습의 다음 단계는 비슷한 도구를 더 외우는 것이 아니라, 한 동작의 성공·실패·재시도 계약을 끝까지 설명하는 것입니다. 예를 들어 “재개가 실패했다”는 한 문장을 요청 취소, Substrate 오류, 준비 확인 시간 초과, 사용자 명령 실패로 분해할 수 있다면 시스템을 층별로 읽기 시작한 것입니다.
자동차의 부품 이름을 다 외우는 것보다 브레이크를 밟았을 때 어느 부품이 어떤 순서로 작동하는지 설명하는 편이 유용합니다. AX에서도 먼저 Task 하나의 생성·재개·중단·삭제를 따라가고, 그 다음에 대량 실행을 생각하세요.
| 용어 | 쉬운 설명 |
|---|---|
| AX | Agent Executor. 에이전트 작업의 환경과 수명주기를 선언형으로 관리하는 실행 플랫폼. |
| 선언형 | 절차를 하나씩 호출하기보다 원하는 설정을 문서로 적는 방식. AX에서도 생성·재개 같은 동작 구분은 필요하다. |
| Task | 격리된 작업 하나의 명세와 상태. 이미지·명령·한도·Workspace 참조를 담는다. |
| Workspace | 작업 전에 준비할 저장소·파일·도구 설정 목록. 이름이 같다고 여러 작업이 한 폴더를 자동 공유한다는 뜻은 아니다. |
| Model | 모델 제공자·식별자·매개변수·인증 정보 참조를 담는 자원. |
| Actor | Substrate가 관리하는 실행 단위. AX는 Task 이름을 Actor 이름으로 사용한다. |
| ActorTemplate | Actor의 이미지·환경변수·볼륨·자원·샌드박스 설정을 담는 실행 틀. |
| Atespace | AX·Substrate 자원의 이름 범위. 기본값은 default이며 Kubernetes namespace와 같은 개념으로 단정하지 않는다. |
| 제어 평면 | 무엇을 실행하고 어떤 상태로 관리할지 결정하는 서버 계층. 실제 사용자 명령의 실행 위치와 구분한다. |
| Reconcile | 선언과 실제 실행 기반을 맞추는 조정. 현재 AX에서는 주요 변경 요청 안에서 직접 호출한다. |
| Phase / Condition | Phase는 Running 같은 단계 요약, Condition은 WorkspaceReady 같은 세부 조건. |
| gRPC 스트리밍 | 한 요청에 응답을 여러 번 보내는 통신 방식. ax watch가 상태 변화를 받는 데 사용한다. |
| 직렬화 | 메모리 안의 데이터를 YAML·JSON·Protobuf 같은 전달·저장 형식으로 바꾸는 일. |
| Pub/Sub | 발행자가 채널로 알림을 보내고 구독자가 받는 방식. AX의 알림은 영속적인 변경 이력과 다르다. |
| 분산 잠금 | 여러 프로세스가 같은 자원을 동시에 변경하지 않도록 조정하는 장치. 장애 조건의 한계도 함께 읽어야 한다. |
| 임대 / TTL | 시간이 지나면 끝나는 소유권과 그 유효 시간. 정상 작업은 주기적으로 연장한다. |
| 멱등성 | 같은 요청을 반복해도 결과가 중복되지 않게 하는 성질. AX의 모든 명령이 같은 재요청 정책을 갖지는 않는다. |
| 백오프 | 실패 직후 무작정 반복하지 않고 간격을 두는 재시도 정책. 지수 백오프는 간격을 점차 늘린다. |
| 샌드박스 / gVisor | 작업을 격리하는 환경과, AX 기본 템플릿이 선택하는 격리 실행 기술. |
| 스냅샷 | 나중에 복원하기 위한 상태 저장본. 파일·볼륨·메모리 중 무엇을 보존하는지 구성을 확인해야 한다. |
| MCP | 모델 기반 앱과 도구·데이터 제공자를 연결하는 프로토콜. AX의 설정 필드 존재와 자동 연결 구현은 구분한다. |
| 스킬 | 에이전트가 참고할 작업 지침·자원 묶음. 현재 기본 준비 코드는 지정한 디렉터리를 만드는 수준이다. |
| Secret 참조 | 명세에 키 본문 대신 Kubernetes Secret의 이름과 키 항목을 기록하는 방법. |
| E2E 테스트 | CLI·서버·저장소·샌드박스를 연결해 사용자 동작 전체를 검증하는 테스트. |
| 의사 버전 | Go 모듈에서 날짜·시간과 커밋 식별자를 포함해 특정 소스를 지정하는 버전 표기. |
| 항목 | 상태 |
|---|---|
| 선정 위치 | GitHub Trending Go Monthly #1, 2026-10-23 분석 |
| 공식 설명 | Google의 공개 에이전트 오케스트레이션 런타임인 Agent Executor(AX) |
| 스타 / 포크 | 14,049 / 732 — 2026-10-23 GitHub 기준 |
| 주 언어 / 생성일 | Go / 2026-03-30 |
| 최신 릴리스 | v0.3.1 — 2026-09-25 |
| 최근 push | 2026-10-22 — GitHub 저장소 활동 시각 |
| 분석 기준 커밋 | 0f085aa — 2026-10-21, 제어 서버에서 받은 명세의 알 수 없는 필드를 실행기가 무시하도록 변경 (#506) |
| Go 코드 규모 | 41파일 / 17,446줄. 구현 7,237 + 테스트 6,751 + 생성 코드 3,458 |
| 그 밖의 코드 | Python 129줄, Protobuf 304줄, Shell 560줄, YAML·YML 723줄 |
| 라이선스 | Apache-2.0 |
| 안정성 안내 | README는 활발한 개발과 안정 릴리스 이전의 큰 호환성 변경 가능성을 명시 |
최근 push 시각과 분석 기준 커밋의 날짜는 서로 다른 정보입니다. 최신 릴리스 v0.3.1의 코드와
이 글이 설명하는 0f085aa의 코드 역시 같다고 가정하지 마세요. 줄 수는 해당 커밋의 물리적 줄 수 집계입니다.
MCP·스킬: README와 개념 문서는 도구·스킬 준비를 강조하지만,
기본 SetupWorkspace()에는 MCP 서버 시작·연결이나 레지스트리 검색 경로가 없습니다.
setupSkills()는 지정 디렉터리를 만들 뿐 스킬 패키지를 내려받지 않습니다.
환경 계획과 모델: Planner.PlanEnvironment()는 존재하지만 기본 서버·실행기의 호출 경로에 연결되어 있지 않습니다.
실제 목표 초기화는 Python 연결 코드를 사용합니다. Go 모델 클라이언트는 Google 제공자를 구현하며,
API 키가 없으면 고정 형식 응답으로 대체하는 경로도 있으므로 계획 응답 자체를 실제 환경 준비의 증거로 보지 마세요.
상태와 복원: 생성은 기본 중단 상태이고, 명령 종료가 자동 완료 상태로 이어지지 않습니다. 준비 상태 확인에서는 일부 초기화 실패가 드러나지 않을 수 있으며, 기본 스냅샷 구성은 볼륨 보존을 선호합니다. 자동 유휴 중단·상태 분기·확장 관측 기능은 로드맵과 실제 구현을 대조해야 합니다.
운영 판단: README의 대규모 실행 목표는 처리량·지연시간·비용 효율의 측정 결과와 구분해야 합니다. E2E 테스트는 기능과 수명주기를 검증하는 구성이며, 대규모 성능을 입증하는 벤치마크는 아닙니다. v1alpha1의 변경 가능성과 Redis 영속성·인증·실행 권한 구성을 고려해 별도의 환경에서 평가하세요.