Index
2026-09-10 — Engineering / Analysis

CaP-X × FurnitureBench: Code-as-Policy 에이전트로 FurnitureSim 평가

FurnitureBench | capgym/cap-x 통합 · gpt-6-astra (Letsur) · one_leg privileged

TL;DR

1/30
v5 실물-충실 API (IK 관절 제어)
3/30
4차 (누설 API + 레시피)
0/30
Astra 3차 (옛 측 규칙)
3/3
에이전트 프로그램 oracle (공식 경로)
6
포팅 결함 발견·수정
1.3° / 6 mm
좋은 낙하 직후 (2/3회)
3/3
스크립트 에이전트 reference

1 배경 / 목적

요청: "Cap-X로 FurnitureBench Eval". CaP-X(NVIDIA·Berkeley·Stanford·CMU, arXiv 2603.22435)는 LLM이 perception/control 프리미티브를 조합하는 Python 코드를 생성해 로봇을 제어하는 Code-as-Policy 벤치마크다. 이 프로젝트의 기존 트랙(pi0.5 0.36, LingBot 0.133)은 모두 학습된 VLA라, 학습 없이 추론만으로 푸는 CaP-X는 one_leg 삽입 병목이 정책 용량 문제인지 기하 추론 문제인지 가르는 대조군이 된다.

전제가 틀렸다: grep -ri furniture의 유일한 히트는 BEHAVIOR의 "rearranging kitchen furniture". CaP-X 39개 태스크 중 FurnitureBench는 0개. "명령 실행"이 아니라 CaP-Gym에 FurnitureSim을 새 시뮬레이터로 통합하는 작업이었다.

2 통합 (어떻게)

하드 블로커 1 — Python. Isaac Gym Preview 4는 gym_36/37/38.so만 제공(python_requires='>=3.6,<3.9'), CaP-X는 >=3.10. 한 프로세스 불가 → py3.8 FurnitureSim 워커 ↔ py3.10 CaP-X 어댑터를 길이 프리픽스 JSON RPC로 연결(numpy는 base64). 하드 블로커 2 — 제어. CaP-X goto_pose는 PyRoKi IK → move_to_joints_blocking인데 FurnitureSim은 OSC delta EE pose만 노출(컨트롤러 osc.py 하나). IK를 우회하고 Cartesian 목표를 OSC delta로 서보. 프레임은 robot-base/WXYZ로 통일(Isaac Gym은 XYZW, EE는 이미 base 프레임, 파트는 world → base 변환).

FurnitureBench/scripts/capx/ protocol.py (양쪽 py에서 import되는 코덱) · furniture_worker.py (py3.8 RPC 서버, OSC 서보) run_worker.sh · eval_capx_furniture.sh (sbm 제출용) · scripted_trace.py (정답 계측) cap-x/capx/envs/simulators/furniture_sim.py FurnitureSimLowLevel(BaseEnv): 워커 spawn + RPC cap-x/capx/integrations/franka/furniture_privileged.py GT 포즈 API (SAM3/GraspNet 불필요) cap-x/capx/envs/tasks/franka/franka_furniture.py 프롬프트 + oracle cap-x/capx/serving/letsur_server.py LLM 프록시 (gw.letsur.ai; 업스트림 400을 읽어 max_tokens→max_completion_tokens, temperature 제거 후 재시도) cap-x/scripts/replay_furniture_trial.py 생성 코드 HD(1280×720, 69.4°) 재생 + 조립 판정 진단 upstream 수정: 등록용 __init__.py 3개만. 나머지는 전부 새 파일.

VLA와 입력 비교. VLA는 5~10 스텝마다 정면·손목 224² 이미지 + 15-D 상태를 받아 delta EE를 낸다. CaP-X(단일턴·privileged)는 에피소드당 1번 ~4.6 KB 텍스트(태스크 설명 + API docstring)만 받고 Python 프로그램을 낸다. 이미지 0장. 코드는 실행 중 get_object_pose()로 시뮬레이터 GT 좌표를 읽으므로 인식 난이도에서는 유리하고, 데모 300개 대신 프롬프트가 곧 데모다.

클러스터 실측. worker pod(bare ubuntu:22.04)에 curl·python3·CA 인증서·libGL 없음 → venv python + certifi + conda glvnd로 우회. 외부 egress는 열려 있음(requests → 401 확인). LETSUR_API_KEY 환경변수는 만료(401), 유효 키는 ~/envs/letsur_key_override.txt. Letsur에 gpt-6-astra·gemini-3.1-pro-preview(논문 기준 모델) 모두 존재.

2b 원본 CaP-X 대비 변경점 (공정성 체크)

git status 기준 수정한 원본 파일은 등록용 __init__.py 3개(+29줄)와 uv.lock뿐. 에이전트 루프(launch.py/base.py 프롬프트 조립 → LLM 1회 → sandbox 실행 → env reward), llm/client.py, privileged 의미(GT 자세를 API로, 이미지 없음)는 원본 그대로.

항목원본 CaP-X (nut_assembly 등)이번 FurnitureBench에이전트에 유리?
시뮬레이터Robosuite/LIBERO/BEHAVIOR, in-processFurnitureSim(py3.8 Isaac Gym) — 별도 프로세스 RPC 브리지중립
로봇 제어goto_pose = pyroki IK → 관절 제어OSC delta-EE 위 Cartesian 서보 자작(적분항·정지 감지·compliant)불리(접촉 제어가 더 어려움)
태스크 프롬프트~10줄, 힌트 3~4문장("손잡이로 잡아라", "rigid transform 재적용")~50줄, 절차를 산문으로(놓아서 안착→재파지→누르며 회전, 재측정, 확인·재놓기, 되감기, compliant, 허용오차)유리
목표 자세get_object_pose("peg")만 — 삽입 자세는 에이전트가 계산get_assembly_slots()가 다리의 최종 채점 자세 그대로, get_target_assembly()가 채점 다리 이름유리
sample_grasp_poseGT 위치 + 고정 쿼터니언 [0,0,1,0]AABB 중심 + 손목 한계 캘리브레이션(q7 ≈ yaw+2.06)으로 파지 측 선택 — 3차 0/30 → 4차 3/30의 원인유리
LLM 엔드포인트OpenRouter 프록시같은 패턴의 Letsur 프록시. gpt-6-astra는 GPT_MODELS에 없어 generic 분기 → reasoning_effort 미전달(게이트웨이 기본값)불명
oracle사람이 쓴 oracle로 환경 검증제 oracle 6종 실패 → 에이전트 trial 05 프로그램을 oracle로 채택(프롬프트엔 미포함)중립
규모trials 100, workers 12trials 30, workers 4 (0.100의 95% CI ≈ 0.02~0.27)중립
요약: 평가 루프·privileged 방식·단일 턴은 원본 그대로, 환경·제어기·API·프롬프트는 전부 새로 만든 것. 그중 프롬프트 힌트 밀도·목표 자세 API·파지 측 규칙 세 가지가 원본 태스크보다 에이전트에게 유리하다. 0.100은 이 조건의 수치.

2c v5 — 실물-충실(real-faithful) 재구성 (2026-09-11)

2b의 "유리" 세 항목을 제거하고 제어를 원본 경로로 맞춘 변형. 사용자 결정: IK → 관절 제어(원본과 동일) + 물리 사실만 담은 프롬프트.

항목4차 (0.100)v5 real-faithful
제어OSC delta-EE 서보 + compliantpyroki IK → 관절 위치 제어. 워커가 팔 7 DOF를 런타임에 PhysX position drive(k=2000, d=60)로 전환; 1 rad/s 보간 + settle. numpy FK(Craig DH)로 IK 잔차 검증 — 시뮬 EE와 0.0000 m 일치
목표 자세get_assembly_slots = 채점기 정답 자세제거. get_holes(part) → 구멍 입구 위치 + 축(CAD 정보). 다리 길이(bbox)로 안착 자세는 에이전트가 구성
채점 다리get_target_assembly()지시문 "attach square_table_leg4"
파지손목 한계 캘리브레이션 상수(q7 ≈ yaw+2.06)상수 제거. 후보 파지(거울 파지 유효 명시) + is_reachable(pose)(IK+FK 잔차+한계) + get_joint_positions
IK—원본 pyroki 서버(api_servers). vel-cost 해가 큰 회전에서 미달(90°가 10° 부족) → 반환 해에서 재해석(≤4회) + 전역 해 fallback을 API에 내장
프롬프트~50줄 절차 레시피 + 벤치마크 내부 문장 3개~20줄: 과제·프레임 규약·물리 사실 4개(나사식이라 돌려야 함, 손에서 밀릴 수 있음, 손목 회전 한계, step 예산). 놓기·재파지 순서·허용오차 전부 삭제 (원문)
워커 pod 함정—robot_descriptions→GitPython이 git 바이너리 요구 → conda env에 git 설치 + GIT_PYTHON_GIT_EXECUTABLE. LLM 프록시가 async def+blocking requests로 4 worker 호출을 직렬화 → 200 s timeout 14/16 trial 무효(118494) → 동기 엔드포인트로 수정, 30 trial 15분(4차 60분)

검증: 손 oracle

로그인 GPU에서 7회 반복 끝에 reward 1.0 (478 step) — 세우기 0.4°, 호버 폐루프 0.7 mm, 입구 3 mm 안에서 놓기 → 안착 +8.3 mm, −90° 나사 4사이클 +8.3 → +6.2 → +4.0 → +1.7 → +0.3 mm, 최종 1.1 mm / 0.0°. 워커 pod 공식 경로(launch.py oracle×3) 2/3(실패 1건은 놓기 직전 0.7°/3.1 mm로 정상 → 놓은 뒤 낙하 확률). OSC 시절 손 oracle 6종 전패와 대조 — 강성 관절 제어가 "누르며 돌리기"에 유리. 놓기 깊이: 입구 4 mm 위에서 놓으면 넘어지고 3 mm 안에 넣고 놓으면 안착 — 프롬프트엔 안 씀.

결과: Astra 30 trial (118577)

실행API성공예외 없이 끝난 trial소요
Astra 4차누설 포함, OSC3/30 = 0.10030/3060분
Astra v5실물-충실, IK 관절1/30 = 0.0332/30 (28건 에이전트 자체 RuntimeError)15분
손 oracle (v5 API)—2/3 (공식 경로)——

Fisher: v5 vs 4차 p = 0.61(유의차 없음), vs LingBot 0.133 p = 0.35, vs pi0.5 0.36 p = 0.0008, vs scripted 0.45 p = 0.0005.

28건의 중단 사유 (에이전트가 스스로 raise한 메시지)

단계n메시지 예
나사·재파지 중 다리 기울어짐/이탈16"The leg tilted during threading", "…tipped during regrasp", "…slipped away from the hole", "…rotated in the wrong threading direction"
삽입 전 정렬 실패7"The threaded end is not aligned with the hole", "…could not be aligned", "The upright leg lost alignment before engagement", "Cannot align the lifted leg above the hole"
도달성 포기5"Required pose is outside the robot's reachable workspace" ×3, "No reachable tightening stroke remains", "The wrist cannot start threading from this grasp"
예외 없이 종료(실패)2trial 01, 23
성공1trial 22 — 조립 등록 후 계속 조이다 "lost axial alignment"로 raise (채점은 이미 완료)
Video 5: v5 실물-충실 API에서의 유일한 성공, trial 22 (코드 code.py, 267줄). 프로그램 길이 218~304줄로 4차(177~239)보다 길고, 30/30이 is_reachable·get_holes를 사용.
해석. 힌트를 걷어내자 실패가 "안 돌림"(4차의 7/30)에서 "돌리다 넘어뜨림"(16/30)으로 옮겨갔다. 강성 관절 제어에서 재파지·−90° 회전은 원본 CaP-X와 같은 조건이지만, 에이전트 프로그램은 넘어짐을 감지하면 재시도 대신 raise로 끝낸다(28/30). 단일 턴이라 그 예외가 곧 trial 종료. 손 oracle이 2/3인 것과의 차이는 (a) 놓기 깊이(입구 3 mm 안)와 (b) 나사 중 재측정·보정 루프의 유무. 4차 대비 성공률 차이는 통계적으로 구분되지 않는다(1 vs 3, p=0.61) — 이 규모에선 "누설 제거로 성능이 떨어졌다"고도 말할 수 없다.

30 trial 계측 재생(sbatch 118663)이 끝나면 4차와 같은 안착 지표 표를 추가.

3 결과

실행job성공코드 생성 / sandbox 에러leg4 조준비고
Astra 1차1170380/3030/30 / 00/30프롬프트가 어느 다리인지 미지정 → 최근접 다리 선택
Astra 2차 (결함 1·2 수정)1170630/3030/30 / 030/30다리 +7.3 cm 들어올림, 기울기 0.0°, 슬롯까지 2.8 cm
Astra 3차 (결함 1~5 수정, 옛 파지 측 규칙)1173370/3030/30 / 030/30세우기 52° 손목 한계(q5/q7 ±2.90)에 갇힘 — 측 규칙 결함 잔존
Astra 4차 (완전 수정 API)1174303/30 = 0.10030/30 / 030/30trial 05 성공: 놓기 +6.3 mm → 나사 5사이클 → 1.0 mm / 0.0°. 나머지는 낙하 불안착·재시도 소진
에이전트 trial 05 프로그램 = oracle (launch.py 공식 경로)login3/3——reward 1.0 ×3, seed 고정 결정적 재현
스크립트 에이전트 (reference)login3/3 (일반적으론 ~0.45)——471~522 step; 낙하 안착 자체가 ~45% (9/20)
Video 1: Astra 2차 trial 11을 HD(1280×720, 69.4° FOV)로 재생. leg4를 집어 5.9 cm 들어올려 수직으로 세우고 슬롯 위로 운반 — 2.8 cm 옆에 내려놓는다(결함 3). 손목 카메라: wrist.mp4
Astra trial 11 contact sheet
Figure 1: 같은 에피소드의 정면 카메라 8프레임.

발견된 포팅 결함 4개 (전부 수정)

#결함증거수정
1채점 대상을 알 수 없음 — should_be_assembled=[(0,4)]라 leg4만 채점, 프롬프트는 "the square table leg"30/30이 list_parts() 최근접 다리, leg4 0건get_target_assembly() 추가 → 2차 30/30 leg4
2sample_grasp_pose가 파트 원점(z=0, 테이블 표면) 반환HD 재생 dz≈0, 다리 밀리기만월드 수직 반-extent 보정 → z=+0.015, dz +0.084
3get_assembly_slots를 "구멍 위치"로 문서화, 실제는 조립 시 다리 원점서로 다른 3 프로그램이 동일 2.8 cm 오차파트가 도달할 완전한 자세(pos+quat) 반환
4나사 조임 부재 (사용자 지적) — 다리는 나사식, insert→pre_grasp→screw_grasp→screw→release 반복 필요1차 프롬프트: "come down onto the hole from directly above"프롬프트에 물리 사실 명시. 프리미티브 screw_in은 만들었다 제거 — 에이전트가 goto_pose로 조합할 몫
1차 프롬프트에 나사 얘기가 한 줄도 없었다. Astra는 시킨 대로 정확히 했고(다리를 0.0° 수직으로 구멍 위에 내려놓음), 그래서 0점이었다. 이후 screw_in(turns=) 프리미티브를 만들어 호출하라고 쓴 것도 잘못 — 에이전트가 풀 문제를 대신 푼 것. 걷어냈다.

나사 조립 실행부 — 스크립트 에이전트 대비 단계별 계측

FurnitureBench 자체 스크립트 에이전트를 상태 전이마다 같은 프레임으로 기록(scripted_trace.py)해 정답을 얻고, oracle을 단계에서 잘라 비교.

단계스크립트 성공 (ep2)스크립트 실패 (ep0)oracle 수정 전oracle 수정 후
놓기 직전 기울기0.6°6.3°2.3°0.8°
놓기 직전 수평 오차1.4 / 1.2 mm12.4 / 7.9 mm1.2 / 2.0 mm1.1 / 0.4 mm
놓는 높이 (조립 위치 대비)+21.8 mm+33.5 mm+39.6 mm+28 mm
놓은 직후 기울기17.2°90° (넘어짐)66~76°14.2°
놓은 직후 구멍축 오차+13.8 mm——+12.6 mm

원인 순서: ① 놓는 높이 4 cm는 튀어 넘어짐 → 2.2~2.9 cm. ② 자유공간 처짐용 서보 적분항이 접촉에서 "테두리 밀어붙이기"가 되어 그립 안에서 피벗(+24 mm에서 19.2°) → goto_pose(compliant=True)(적분 off, 1 cm 스텝, 회전 3°/tick) 추가 → 0.8°. ③ 재파지는 정상, -95° 비틀기 중 다리가 뽑힘 — 그러나 같은 낙하 코드가 회차마다 구멍 안(G) / 누움(I) / 옆에 섬(J)으로 갈려, 단일 에피소드로는 비틀기를 판정할 수 없다는 것이 드러났다.

variant J drop
Figure 2: 변형 J의 낙하(틱 250→340). 놓은 뒤 다리가 소켓 옆에 서 있다 — 넘어진 게 아니라 옆으로 빗나간 세 번째 실패 양상. 영상: varJ.mp4, 성공 낙하: probeG.mp4
variant H twist
Figure 3: 변형 H 첫 사이클(틱 390→465). 재파지까지 서 있던 다리가 비틀기 중 기울어 465에서 누워 있다.
낙하 안착은 정답과 일치: 수정 후 oracle의 놓은 직후 상태(14.2°, +12.6 mm)가 스크립트 성공 에피소드(17.2°, +13.8 mm)와 같다. 스크립트 에이전트는 바로 이 상태에서 5사이클 나사(사이클당 1~2 mm 하강, 29→14→1.3→…→0°)로 조립한다.
낙하 분산 3회 (동일 코드): 90° 넘어짐 / 1.3°·6.1 mm / 1.3°·6.1 mm(2·3회 0.1 mm까지 동일). 낙하가 되면 조립 위치 6 mm 위에서 거의 다 들어간다 — 스크립트 성공의 낙하 직후(17°/+13.8 mm)보다 낫다. 남은 건 압입 몇 mm뿐이고, 비틀기는 오히려 뽑아낸다.
결함 5 — sample_grasp_pose yaw: 오일러 "xyz"[2]로 뽑은 yaw가 옆으로 누운 파트에서 짐벌 분기가 뒤집혀 손목이 45° 기운 grasp를 만들고, 그 transform으로 계산한 호버 목표가 테이블 밖으로 갔다(변형 L: 다리를 1.34 m 날림). 장축의 수평 투영에서 yaw를 잡도록 수정(닫힘 축 = 그리퍼 X, 스크립트 정책의 reset 파지에서 역산), 5개 자세 검증 통과.
호출 단위 트레이스 (변형 N): #03 close_gripper grip 0.017, #14 0.003 — 다리 폭 0.030인데 손가락이 지나쳐 닫힘 = 파지 실패, 이후 빈 손으로 "놓기" 수행. 3번째에 잡힌 뒤 호버(강성 서보)는 2.0° / 2.2 mm로 완벽했으나, 호버에서 채점 yaw로 돌리는 컴플라이언트 회전이 28° / 26 mm로 망쳤다. 결함 5의 첫 수정(+90°)이 틀렸던 것 — 8.75 cm 길이 방향으로 닫으면 6.5 cm 개방을 넘어 못 잡는다(회귀 프로브 90°/22 cm 재확인). 정정: yaw = atan2(장축). 그간 "종료 90°" 실행 대부분이 파지 실패 → 빈 손 → 재시도 폭주였을 가능성이 크고, 나사/누르기 루프는 아직 안착된 다리 위에서 한 번도 시험되지 않았다.
변형 O — 소켓 안착 7.0 mm까지 (16:25): atan2 yaw로 첫 파지 grip 0.030 ✓. 3번째 시도에서 강성 yaw 회전 → 1.9°/0.9 mm, 컴플라이언트 하강 → 0.8°/2.0 mm, 놓기 → 2.3°/2.2 mm, dy +6.7 mm. 최종 pos error 7.0 mm·tilt 2.9°·yaw 정렬. 누르기 루프는 oracle 판정 반경(8 mm)이 벤치마크 구멍축 허용(5 mm)보다 느슨해 1.6 mm 남기고 조기 종료 — 판정을 축별로 정확히 미러링한 P2 대기 중. 1·2번째 시도는 세우는 동안 그립 내 미끄러짐(호버 51.7°, 또는 yaw 회전 후 60 mm) → 호버에서 grasp transform 재측정·재서보하는 P 실행 중.
세우기 불가 자세 (16:40): P·M‴ 트레이스 — 첫 파지 ✓ 후 호버에서 tilt 51.6°, lat 2 mm, 재측정·재서보 3회에도 51.6° 고정. 슬립이 아니라 그 파지 자세의 손목 회전을 OSC가 도달 못 하는 것(위치만 수렴). 4개 대칭 후보를 가까운 순으로 시도해 tilt<10°인 첫 후보를 채택하는 변형 Q 큐.
Astra 3차 평가 제출 — job 117337 (17:05): 결함 5개 수정 + 프롬프트(나사·낙하·재측정·컴플라이언트 조언) + 제어(컴플라이언트 모드, 서보 예산 600틱)가 반영된 환경에서 gpt-6-astra × one_leg 30 trial 재실행 (own QOS, 1 GPU, 4 workers, ~25분). 결과는 호출별 트레이스로 재생해 성공률뿐 아니라 안착 지표(기울기·mm)까지 기록. 참고: 그간의 변형 I~Q는 LLM 없이 제 oracle을 검증한 것이며 CaP-X 평가가 아니다.
프로브 S — 세우기 정체는 관절 한계 (17:15): 서보 예산 3배에도 호버 tilt 52.0° 고정. 관절각: q5 = −2.90, q7 = +2.90 = Panda 한계(±2.8973)에 두 관절이 못 박힘. 이 파지 자세에서 세우기 롤은 기구학적으로 불가. 수정: sample_grasp_pose가 장축 가로지름 yaw 두 후보(θ, θ+180°) 중 현재 손목 yaw에 가까운 쪽을 택해 회전 여유 확보(프로브 T 검증 중). Astra 3차에는 미반영(이미 import).
프로브 T = S 동일 재현 → 파지 측 선택 규칙 교체 (17:25): "현재 손목 yaw에 가까운 쪽"은 같은 쪽을 골랐다. 관절각 산술: 파지 시 q7 = 1.97, 세우기 롤 +0.93 → 2.90(한계). 반대쪽(θ+180°)이면 −1.17 → 롤 후 −0.24. 규칙을 "파지 후 예측 |q7| 최소"로 교체.
프로브 V — 반대쪽 파지로 세우기 성공 (17:45): 파지 시 q7 +1.97(atan2 쪽) vs −1.16(반대쪽); 호버 기울기 52.0° vs 1.8° / 1.7 mm(첫 후보에서 수렴). 툴 yaw +Δ → q7 −Δ로 부호 확정, API 규칙 수정. Astra 3차는 옛 규칙으로 실행 중 → 4차 필요.
Astra 3차 trial 1 트레이스 — 에이전트가 안착까지 (17:55): 1차 시도는 정체 측(34~40°)이라 놓쳤지만, 프롬프트대로 넘어진 다리를 다시 집어 호버 1.5°/2.9 mm → 컴플라이언트 하강 1.0°/0.9 mm → 놓기 1.4°, 0.4 mm, dy +6.2 mm(판정 5 mm까지 0.8 mm). 누르기용 재파지가 grip 0.003(빈 손) — 결함 6: sample_grasp_pose가 세워진 다리의 꼭대기(원점+4.4 cm)를 가리킴. AABB 중심 + 1.5 cm 클램프로 수정. 에이전트의 낙하·재시도·컴플라이언트·재측정 사용은 모두 정확했다.
캘리브레이션 — q7 ≈ tool yaw + 2.06 (18:15): 홈 EE yaw는 0이 아니라 −0.932 rad. 측정 q7(A +1.97 / B −1.16)이 후보 yaw(−0.091 / +3.05)와 q7 ≈ yaw + 2.06으로 두 점 모두 0.01 이내 일치. "손목 회전 최소"(A +1.02 vs B −2.12)는 정반대를 고르는 오답이었다. 규칙: 후보 yaw 중 −2.06에 가까운 쪽(캘리브레이션 상수, 근거 명시). 동작 프로브 Y 통과 시 v4 자동 제출.
측 규칙 확정 · v4 제출 · 에이전트 프로그램 6.3 mm (18:40): 프로브 Y — API 기본 파지 q7 −1.11, 호버 2.2° / 3.1 mm → Astra 4차 job 117430. 프로브 Z — 에이전트 trial-1 코드가 완전 수정 API로 첫 시도에 3.0°, 2.4 mm, dy +6.4 mm 안착, 재파지 0.030 ✓, 최종 dy +6.3 mm(판정까지 1.3 mm). 낙하 직후는 항상 +6.1~6.7 mm이고 누르기로는 더 안 내려간다 — 마지막 6 mm는 나사산, 회전이 필요(사용자 최초 지적). 누르기-전용 설계 폐기, 컴플라이언트 소각 비틀기 사이클(AA) 시험 중.
AB — 컴플라이언트 30°/3 mm 나사 사이클은 무효 (19:10): 낙하 14.9°/11.9 mm/+13.2(스크립트 성공형)를 수용하고 재파지도 잡혔지만 3사이클 동안 dy +12.3 → +11.9 mm, 기울기 ~18° 불변. 스크립트 screw는 강성 OSC 절대 목표 −95°+5 mm 압입으로 첫 사이클에 17→1.3°, 13.8→6.4 mm. 예전 강성 비틀기 실패는 재파지 yaw 버그 상태였으므로 반증이 아니다.
AC — 기운 툴 축 둘레의 비틀기가 다리를 뽑음 (19:25): 재파지 ✓(19°) → 강성 −90°·5 mm → 27.6°/22.7 mm → 놓자 73.5°. 컴플라이언트 재파지가 기운 다리를 따라 툴이 기운 채 잡혀, 비틀기가 그 축 둘레로 돌며 밑면을 소켓 밖으로 끌어냄. 스크립트 pre_grasp는 절대 수직·강성 — 손가락이 다리를 세우고 비틀기는 수직축 둘레.
🎯 Astra 4차 — 첫 조립 성공 (19:40, 5/30 시점): trial_05 reward 1.0. 완전 수정 API에서 CaP-X 에이전트가 one_leg를 조립했다. 생성 코드(211줄)의 주석: "The grasp may slip during the large rotation. Measure it again." · "Regrasp from above with a neutral wrist for each bounded turn." · "Rotate about the measured leg axis, not the gripper origin. Two compliant substeps keep contact loads and wrist travel small." — 제 oracle이 실패한 지점(툴 축 둘레 비틀기)을 에이전트는 스스로 피했다. 영상 · 코드 · 프롬프트. 최종 성공률은 30 trial 완료 후.
AD 실패 → oracle 탐색 종료 (19:55): 강성 수직 재파지도 다리를 세우지 못했고(18°), 강성 −90° 비틀기에 팔이 크게 재구성되며 밑면을 끌어냄(25.3°/20.6 mm → 90°). 제 oracle 계열(N~AD)은 낙하 안착(+6 mm)까지만 재현. 에이전트 성공 코드("측정된 다리 축 둘레·컴플라이언트 서브스텝·재측정")를 참조 oracle로 채택(출처 주석) — CaP-X의 oracle_code는 동작 검증된 참조 해이므로 정당.
Video 2: Astra 4차 trial 05 — 조립 성공을 HD로 재생. 놓기 1.5°/1.8 mm/+6.3 mm → 나사 사이클마다 +6.3 → +4.4 → +2.1 → +0.3 mm(사이클당 ~2 mm, 스크립트 정책과 동일) → 최종 0.0°/1.0 mm. 손목: wrist.mp4
Video 3: Astra 4차 trial 18 성공 (평가 시 저장된 저해상도 영상, 코드 code.py)
Video 4: Astra 4차 trial 21 성공 (코드 code.py)
공식 경로 재현 (20:20): launch.py --use-oracle-code(모듈 oracle = Astra v4 trial 05 프로그램) 3/3 reward 1.0 (Task Completed True ×3). 환경·API가 CaP-X 공식 경로로 조립 가능함이 재현 가능하게 확정 — 참조 프로그램은 에이전트 자신의 코드.
최종 30-trial 결과: Astra 4차(완전 수정 API) 3/30 = 0.100 · 3차(옛 측 규칙) 0/30. Fisher exact (양측): 3/30 vs pi0.5 0.36 (18/50) p = 0.017 — pi0.5가 유의하게 높음; vs pi0.5 초기 0.24 (12/50) p = 0.15 — 유의차 없음; vs LingBot 0.133 (4/30) p = 1.00 — 동급; vs 스크립트 정답 0.45 (9/20) p = 0.007. 데모 0개 단일 턴 코드 정책이 demo-fine-tuned LingBot과 동급, pi0.5 최종 모델에는 못 미친다.

베이스라인 비교 (FurnitureSim one_leg, low randomness)

정책입력학습 데이터성공률n
pi0.5 (fine-tuned)이미지 2뷰 + 관절scripted demo0.24 / 0.3650 / 50
LingBot-VA이미지 + 관절scripted demo0.133 / 0.030 / 80
스크립트 에이전트 (정답 정책)GT 자세—~0.4520
CaP-X gpt-6-astra (privileged, 단일 턴)텍스트 프롬프트 + API 문서 + GT 자세 (이미지 없음)0 demo, 0 fine-tune0.10030

4차 30 trial 안착 지표 (계측 재생, seed 0, sbatch 117532/117533)

trial평가 성공step최대 들어올림 mm최소 기울기 °최소 수평 오차 mm최종 기울기 °최종 자세 오차 mm재생 성공유형
0103000911.11.17.515.8예산 소진, 세워졌으나 이탈
0205391380.60.43.06.5자체 완료 판정 조기 종료 (~6.5 mm)
03030001002.41.190.0378.0다리 넘어짐 (90°)
04029441411.01.383.452.5다리 넘어짐 (90°)
05118981770.90.60.01.3✓성공
0603000931.00.20.01.2✓재생에선 성공 (낙하 확률)
07028771021.00.790.0735.0다리 넘어짐 (90°)
08030001430.62.113.081.4예산 소진, 세워졌으나 이탈
09030001430.51.490.045.6다리 넘어짐 (90°)
1001738521.00.80.00.8✓재생에선 성공 (낙하 확률)
11030001290.53.590.079.8다리 넘어짐 (90°)
120528772.53.93.06.8자체 완료 판정 조기 종료 (~6.5 mm)
13030001120.92.090.0675.3다리 넘어짐 (90°)
14030001700.20.70.061.5예산 소진, 세워졌으나 이탈
15030001710.00.74.599.1예산 소진, 세워졌으나 이탈
160534851.01.73.16.6자체 완료 판정 조기 종료 (~6.5 mm)
17030001431.11.890.01489.8다리 넘어짐 (90°)
18127781450.91.00.01.0✓성공
19030001000.52.390.0134.6다리 넘어짐 (90°)
20027921172.41.890.073.0다리 넘어짐 (90°)
2111852851.11.20.01.3✓성공
22019781380.51.10.76.4자체 완료 판정 조기 종료 (~6.5 mm)
2303000831.01.690.0813.8다리 넘어짐 (90°)
2403000902.40.590.01714.8다리 넘어짐 (90°)
250665901.00.51.46.4자체 완료 판정 조기 종료 (~6.5 mm)
2603000992.42.390.092.7다리 넘어짐 (90°)
2705671231.00.93.16.6자체 완료 판정 조기 종료 (~6.5 mm)
280550601.62.43.06.7자체 완료 판정 조기 종료 (~6.5 mm)
2903000911.12.290.0325.3다리 넘어짐 (90°)
30026621202.52.090.061.6다리 넘어짐 (90°)
결과 유형 (30 trial)n설명
성공3trial 05·18·21. 재생에서도 3/3 재현
다리 넘어짐 (최종 90°)14나사 사이클·재파지 중 다리가 튕겨 눕고, 이후 회복 실패(대부분 3000 step 소진)
자체 완료 판정 조기 종료7528~1978 step에 종료, 최종 6.4~6.8 mm(= 낙하 직후 잔차). 프로그램의 "조립됨" 판정이 ‖p−target‖ < 7~8 mm라 낙하만으로 통과 → 나사 루프가 돌지 않음. 채점은 구멍 축 ≤ 5 mm
예산 소진, 세워졌으나 이탈4기울기 0~13°이나 자세 오차 16~99 mm — 구멍 옆에 서 있음
재생에선 성공2trial 06·10: 평가 땐 실패, 같은 코드 재생은 reward 1.0 — 낙하 안착 확률(같은 코드가 갈림)
가장 싼 개선 = 프롬프트의 허용오차를 정확한 값으로. 7/30이 낙하 잔차 6.5 mm를 "완료"로 판정해 나사를 한 번도 돌리지 않았다(사용자가 처음 지적한 "꽂기만 하고 안 돌린다"가 이 유형). 원인은 제 프롬프트 문장 "Assembly is registered at roughly 5-10 mm of position error" — 에이전트가 이 범위에서 7~8 mm를 골랐고, 낙하만으로 6.5 mm가 되니 통과했다. 실제 채점은 축별 [10, 5, 10] mm(구멍 축 5 mm)라 "낙하 직후는 ~6 mm 위에 걸리며 5 mm 안으로는 나사로만 내려간다"고 써야 한다. 30 trial 전원 세우기·호버(≤2.5°, ≤3.9 mm)까지는 도달하므로 병목은 접촉 단계뿐이다.

주의: 재생은 같은 코드·seed 0이지만 물리가 완전히 결정적이지 않아 평가 결과와 다를 수 있다(성공 3/3 재현, 실패 2/27은 재생에서 성공). 원본 TSV: astra_v4_summary_all30.tsv.

4 Takeaway

의미

최종 숫자는 Astra 4차 3/30 = 0.100 (1·2차 0/30, 3차 0/30은 환경 결함이 만든 값이라 벤치마크 수치가 아니다). 데모 0개·fine-tune 0으로 텍스트 프롬프트와 API 문서만 받은 단일 턴 코드 정책이 pi0.5(demo 수백 개 fine-tune) 0.36 / LingBot 0.133 옆에 놓인다. Astra의 계획·기하 추론은 사람이 짠 oracle보다 나았다(grasp transform 재측정, 손목 회전 최소 자세 선택, z_approach 축 인지) — 그리고 제가 손으로 만든 나사 변형 6종(AA~AD 등)이 전부 실패한 나사 조임을 에이전트 프로그램이 처음으로 성공시켰다.

에이전트가 받은 것 vs 제가 준 것 (귀속). Astra의 입력은 텍스트 8.7 KB 한 번뿐이다 — 시스템 문장 1줄 + 제가 쓴 태스크 지시(~50줄) + 제가 쓴 API docstring. 이미지 없음, 예시 코드 없음, oracle 없음, 피드백 턴 없음; 30 trial은 같은 프롬프트의 독립 샘플(temperature 1)이고 실행 중 GT 자세는 API 호출로 읽는다. 대신 제 지시문에는 4번의 실패를 보고 쓴 전략 힌트가 들어 있다: 밀어 넣지 말고 놓아서 안착 → 재파지 후 누르며 돌리기, 세운 뒤 자세 재측정, 낙하 후 확인·재놓기, 손목 한계 때문에 잡고-놓고-되감기, 접촉 구간은 compliant, 허용오차 "5~10 mm / 20°". API도 답을 상당히 품고 있다: get_target_assembly가 채점 다리를 지목하고, get_assembly_slots가 다리의 최종 목표 자세를 그대로 주며, sample_grasp_pose가 손목 한계 캘리브레이션으로 파지 측을 고른다(이 규칙 하나가 3차 0/30 → 4차 3/30). 즉 0.100은 "Astra 단독"이 아니라 "Astra + 제 프롬프트/API"의 수치다. 에이전트가 스스로 한 것은 그 산문을 기하·코드로 바꾼 부분 — 프레임/쿼터니언 변환, grasp transform 측정, 손목이 아니라 다리 축을 중심으로 회전하는 명령 합성, 되감기 루프, 상태 확인 분기.

CaP-X에서는 프롬프트가 곧 데모다. VLA에 데모 300개가 "나사식"을 가르치듯, 여기선 프롬프트의 한 줄이 그 역할을 한다. 그 한 줄이 빠지면 성공률이 구조적으로 0이 된다 — 그래서 나오는 숫자는 모델 능력만큼 프롬프트 완성도의 함수다.

FurnitureBench one_leg의 난이도는 "삽입"이 아니라 "낙하 안착 확률 + 나사 조임"이다. 정답 정책조차 낙하가 동전 던지기(~45%)이고, 그 뒤 5 사이클의 접촉 제어가 필요하다. 열린 루프 코드 정책이 이걸 풀려면 get_object_pose로 상태를 읽어 재시도하는 폐루프가 필수이며, 그건 code-as-policy가 원래 시험하려는 능력이다.

5 Next Steps

미해결

(해결됨: 나사 실행부 — 에이전트 프로그램이 조립, oracle 3/3.) 남은 병목은 낙하 안착 확률(~45%)과 안착 실패 후 재시도 예산(3000 step) 소진. 이전 기록: 낙하 후 구멍 안·수직을 확인하고 아니면 다시 놓는 폐루프(변형 L/M) + 컴플라이언트 -45° 비틀기. 검증되면 모듈 oracle·프롬프트 반영 완료 상태.

다음 실험

① multi-turn(피드백 포함) 모드에서 Astra 재평가 — 단일 턴은 실행 중 관측을 프롬프트로 되돌려 받지 못한다. ② max_steps 3000→6000으로 재시도 예산 영향 확인. ③ 낙하 안착 확인 후 재놓기를 프롬프트 지침이 아닌 관측 기반으로 에이전트가 짜는지 trial별 코드 분석. ④ gemini-3.1-pro-preview로 같은 config — 논문 기준 모델과 직접 비교. ③ 멀티턴(M1–M4, visual differencing) — VLM_MODELS에 astra 추가 + 카메라 intrinsics/extrinsics 노출 필요. ④ lamp/round_table/cabinet은 등록만 됨, 가구별 프롬프트 필요.

운영 교훈

pgrep -f/grep 가드가 자기 셸이나 다른 살아있는 셸의 명령줄 텍스트를 매칭해 교착·자기-kill 4회. 가드는 실제 런칭 줄에만 나타나는 --out-dir …/diag_<name>으로 걸 것.