Ironclaw — 코어 루프 분석
Table of Contents
1. 이중 엔진 아키텍처
2. V1 에이전트 루프 (Rust)
3. V2 오케스트레이터 (Python on Monty VM)
4. 루프 보호 메커니즘
5. 자기 수리 시스템
6. 3계층 메모리 아키텍처
7. 자기 수정 오케스트레이터
8. 종합 인사이트
1. 이중 엔진 아키텍처
Ironclaw는 동일한 에이전트 루프를 두 가지 엔진으로 구현한다. 이는 점진적 진화 전략이다.
V1 Engine (Rust) V2 Engine (Python on Monty VM)
───────────────────────── ─────────────────────────────────
src/agent/agentic_loop.rs crates/ironclaw_engine/orchestrator/default.py
전통적 LLM → 도구 → 반복 Python 코드가 Monty VM 위에서 실행 LoopDelegate 트레이트로 추상화 호스트 함수(llm_complete, execute_code_step) 3가지 소비자: 채팅/작업/컨테이너 Rust가 제공하는 런타임 위에서 동작
고정된 루프 로직 런타임에 자기 수정 가능 컴파일 타임에 결정 에이전트가 오케스트레이터 코드를 수정 가능
2. V1 에이전트 루프 (Rust) V1
src/agent/agentic_loop.rs2.1 run_agentic_loop() — 메인 루프
pub async fn run_agentic_loop(
delegate: &mut dyn LoopDelegate,
reason_ctx: &mut ReasoningContext,
config: &LoopConfig,
) -> Result<LoopOutcome> {
let mut iteration_state = IterationState::new();
let mut dup_tracker = DuplicateToolCallTracker::new(); // 중복 도구 호출 감지
// ═══════════════════════════════════════════════════════
// 메인 루프: max_iterations (기본 50) 까지 반복
// ═══════════════════════════════════════════════════════
for iteration in 1..=config.max_iterations {
// ── (1) 신호 확인: 취소, 중단, 메시지 주입 ──
if let Some(signal) = delegate.check_signals().await {
match signal {
LoopSignal::Stop => return Ok(LoopOutcome::Stopped),
LoopSignal::InjectMessages(msgs) => {
reason_ctx.messages.extend(msgs);
}
}
}
// ── (2) 사전 LLM 호출 준비: 비용 가드, 도구 갱신 ──
delegate.before_llm_call(reason_ctx).await?;
// ── (3) LLM 호출 ──
let response = delegate.call_llm(reason_ctx, config).await?;
// ── (4) 응답 유형별 분기 ──
match response {
// 텍스트 응답 → 종료할지 판단
RespondResult::Text(text) => {
match handle_text_response(&text, &mut iteration_state, config) {
TextAction::Return => {
return Ok(LoopOutcome::Completed(text));
}
TextAction::NudgeToolUse => {
// "도구를 실제로 호출하세요" 메시지 주입
reason_ctx.messages.push(tool_intent_nudge_message());
iteration_state.tool_intent_nudge_count += 1;
}
TextAction::ForceText => {
// 너무 많은 넛지 후 → 텍스트로 강제 종료
return Ok(LoopOutcome::Completed(text));
}
}
}
// 도구 호출 → 실행 후 결과를 컨텍스트에 추가
RespondResult::ToolCalls(tool_calls) => {
// 잘린 도구 호출 처리 (finish_reason == Length)
if response.finish_reason == FinishReason::Length {
iteration_state.truncated_count += 1;
if iteration_state.truncated_count >= 3 {
// 3회 연속 잘림 → 강제 텍스트 모드
config.force_text = true;
continue;
}
// 잘린 도구 호출 폐기, 다른 접근 유도
reason_ctx.messages.push(truncated_warning_message());
continue;
}
// 중복 도구 호출 검사
let dup_result = dup_tracker.check(&tool_calls);
match dup_result {
DupAction::Allow => {}
DupAction::Warn => {
// 3회 연속 동일 실패 → 경고 주입
reason_ctx.messages.push(duplicate_warning_message());
}
DupAction::ForceText => {
// 5회 연속 → 강제 텍스트 모드
config.force_text = true;
continue;
}
}
// ── 도구 실행 ──
let results = delegate.execute_tool_calls(&tool_calls).await?;
// ── 피드백 지점: 결과를 컨텍스트에 추가 ──
for result in results {
reason_ctx.messages.push(result); // ← 다음 LLM 호출의 입력이 됨
}
}
}
// ── (5) 반복 후 처리 ──
delegate.after_iteration(iteration, &iteration_state).await?;
}
// max_iterations 도달
Ok(LoopOutcome::MaxIterationsReached)
}
Insight: LoopDelegate 트레이트 — 하나의 루프, 세 가지 소비자
LoopDelegate 트레이트를 통해 채팅(대화형), 작업(백그라운드), 컨테이너(격리) 세 가지 실행 모드가 동일한 루프 코드를 공유한다. 각 모드는 check_signals(), before_llm_call(), execute_tool_calls()만 다르게 구현. 이는 Pi의 AgentLoopConfig 패턴과 구조적으로 동일하지만, Rust 트레이트로 더 엄격하게 타입 체크된다.
3. V2 오케스트레이터 (Python on Monty VM) V2
crates/ironclaw_engine/orchestrator/default.py3.1 run_loop() — Python 오케스트레이터
def run_loop(context, goal, actions, state, config):
"""V2 에이전트 루프. Monty VM 위에서 실행되는 Python 코드.
호스트 함수(__llm_complete__, __execute_code_step__ 등)는
Rust 런타임이 제공한다."""
max_iterations = config.get("max_iterations", 30)
step_count = state.get("step_count", 0)
# 첫 스텝에서 문서 검색 + 스킬 목록 주입
if step_count == 0:
# 이전 스레드의 지식을 현재 스레드에 자동 주입
docs = __retrieve_docs__(goal, 5) # 목표 기반 관련 문서 검색
skills = __list_skills__()
if docs:
context.append({"role": "system", "content": format_docs(docs)})
if skills:
context.append({"role": "system", "content": format_skills(skills)})
working_messages = list(context)
# ═══════════════════════════════════════════════════════
# 메인 루프
# ═══════════════════════════════════════════════════════
for step in range(step_count, max_iterations):
# (1) 정지 신호 확인
signal = __check_signals__()
if signal == "stop":
return {"status": "stopped"}
# (2) 예산 확인 (토큰/시간/비용)
budget_status = __check_budget__()
if budget_status.get("exceeded"):
return {"status": "budget_exceeded", "reason": budget_status["reason"]}
# (3) 컨텍스트 압축 — 85% 임계치
if should_compact(working_messages, config):
working_messages = compact_if_needed(working_messages, state, config)
# (4) LLM 호출
response = __llm_complete__(working_messages, actions)
# (5) 응답 유형별 처리
response_type = response.get("type")
if response_type == "text":
text = response["content"]
# FINAL() 추출 — 명시적 종료 신호
final_answer = extract_final(text)
if final_answer is not None:
return {"status": "completed", "answer": final_answer}
# 도구 의도 넛지: "검색해볼게요"라고만 하고 안 하면
if has_tool_intent(text) and state.get("nudge_count", 0) < 2:
working_messages.append({
"role": "user",
"content": "Please use the available tools to take action, "
"don't just describe what you would do."
})
state["nudge_count"] = state.get("nudge_count", 0) + 1
continue
# 실행 의무 체크
if state.get("obligation_enabled"):
working_messages.append({
"role": "user",
"content": "You must take action using tools. Do not just respond with text."
})
continue
# 일반 텍스트 → 종료
return {"status": "completed", "answer": text}
elif response_type == "code":
# CodeAct: 코드 블록 직접 실행
result = __execute_code_step__(response["code"])
output = format_output(result)
working_messages.append({"role": "User", "content": output})
# 연속 에러 추적
if result.get("error"):
state["consecutive_errors"] = state.get("consecutive_errors", 0) + 1
if state["consecutive_errors"] >= config.get("max_consecutive_errors", 5):
return {"status": "failed", "reason": "max_consecutive_errors"}
else:
state["consecutive_errors"] = 0
elif response_type == "actions":
# 구조화된 도구 호출 (여러 개 병렬 실행)
results = __execute_actions_parallel__(response["actions"])
for result in results:
working_messages.append({
"role": "ActionResult",
"content": format_action_result(result)
})
# 상태 저장 (재개 가능)
state["step_count"] = step + 1
state["working_messages"] = working_messages
# max_iterations 도달
return {"status": "max_iterations"}
Insight: V1과 V2의 핵심 차이 — 3가지 응답 유형
V1은 Text | ToolCalls 2가지 분기만 있지만, V2는 text | code | actions 3가지. code는 CodeAct 패턴으로, LLM이 Python 코드 블록을 직접 출력하면 실행하는 방식. 이는 도구 호출의 JSON 오버헤드를 줄이고, 복잡한 로직을 코드로 직접 표현할 수 있게 한다.
3.2 FINAL() — 명시적 종료 신호
def extract_final(text):
"""LLM 출력에서 FINAL("답변") 패턴을 추출.
다양한 형태를 모두 파싱한다."""
# 지원하는 형태들:
# FINAL("답변")
# FINAL('답변')
# FINAL(\"\"\"여러 줄 답변\"\"\")
# FINAL(따옴표 없는 답변)
# FINAL(중첩(괄호)가 있는 답변)
patterns = [
r'FINAL\("([^"]*?)"\)', # 쌍따옴표
r"FINAL\('([^']*?)'\)", # 홑따옴표
r'FINAL\("""(.*?)"""\)', # 삼중따옴표
r'FINAL\(([^)]*)\)', # 미따옴표
]
for pattern in patterns:
match = re.search(pattern, text, re.DOTALL)
if match:
return match.group(1).strip()
# 중첩 괄호 처리
if "FINAL(" in text:
return extract_balanced_parens(text)
return None
V1 vs V2 종료 방식의 차이
V1: 모델이 도구를 호출하지 않으면 종료 (Pi와 동일한 암묵적 종료)V2: 모델이
FINAL("답변")을 명시적으로 출력해야 종료V2의 접근이 더 안정적이다 — 모델이 중간에 "생각 중" 텍스트를 출력해도 루프가 종료되지 않는다. 단, 모델이 FINAL()을 잊으면 max_iterations까지 돌 수 있다는 트레이드오프.
4. 루프 보호 메커니즘
src/agent/agentic_loop.rs (160-336행)4.1 DuplicateToolCallTracker — 동일 실패 반복 방지
struct DuplicateToolCallTracker {
history: VecDeque<ToolCallFingerprint>,
consecutive_duplicates: usize,
}
impl DuplicateToolCallTracker {
fn check(&mut self, tool_calls: &[ToolCall]) -> DupAction {
let fingerprint = compute_fingerprint(tool_calls);
if self.history.back() == Some(&fingerprint) {
self.consecutive_duplicates += 1;
} else {
self.consecutive_duplicates = 0;
}
self.history.push_back(fingerprint);
match self.consecutive_duplicates {
0..=2 => DupAction::Allow, // 1-2회: 허용
3..=4 => DupAction::Warn, // 3-4회: 경고 주입
_ => DupAction::ForceText, // 5회+: 강제 텍스트 모드
}
}
}
4.2 잘린 도구 호출 처리
// LLM 출력이 max_tokens에 의해 잘린 경우
// finish_reason == Length → 도구 호출 JSON이 불완전
if response.finish_reason == FinishReason::Length {
iteration_state.truncated_count += 1;
if iteration_state.truncated_count >= 3 {
// 3회 연속 잘림 → 도구가 너무 큰 출력을 만드는 것
// 강제 텍스트 모드로 전환
config.force_text = true;
continue;
}
// 잘린 도구 호출 폐기 → 다른 접근 유도
reason_ctx.messages.push(Message {
role: "system",
content: "Your previous tool call was truncated due to output length. \
Please try a different approach with shorter arguments, \
or break the task into smaller steps."
});
}
4.3 도구 의도 넛지 (Tool Intent Nudge)
// LLM이 "검색해볼게요", "파일을 확인하겠습니다" 같은 텍스트만 출력하고
// 실제 도구를 호출하지 않는 경우
const TOOL_INTENT_NUDGE: &str = "You expressed intent to use tools but didn't \
actually call any. Please use the available tools to take action \
rather than describing what you would do.";
fn handle_text_response(
text: &str,
state: &mut IterationState,
config: &LoopConfig
) -> TextAction {
// 도구 의도가 감지되고, 넛지 횟수가 한도 미만이면
if has_tool_intent(text)
&& state.tool_intent_nudge_count < config.max_tool_intent_nudges // 기본 2
{
return TextAction::NudgeToolUse;
}
// 넛지 한도 초과 → 그냥 텍스트 응답으로 종료
if state.tool_intent_nudge_count >= config.max_tool_intent_nudges {
return TextAction::ForceText;
}
TextAction::Return
}
Insight: 4개 시스템 중 ironclaw만 가진 방어 메커니즘
중복 도구 호출 감지, 잘린 도구 호출 처리, 도구 의도 넛지 — 이 세 가지는 Pi, Hermes, OpenClaw 어디에도 없다. 이는 ironclaw가 자율 실행(무인 운영)을 목표로 하기 때문. 사람이 터미널 앞에 앉아있으면 무한 반복을 직접 중단하면 되지만, 무인 실행에서는 시스템이 스스로 감지하고 탈출해야 한다.
5. 자기 수리 시스템 SELF-REPAIR
src/agent/self_repair.rspub struct RepairTask {
stuck_threshold: Duration, // 멈춤 판단 시간
max_repair_attempts: usize, // 최대 복구 시도 횟수
check_interval: Duration, // 감시 주기
}
impl RepairTask {
/// 백그라운드에서 주기적으로 실행되는 감시 루프
pub async fn run(&self, db: &Database) {
loop {
sleep(self.check_interval).await;
// ── (A) 멈춘 작업 감지 및 복구 ──
let stuck_jobs = db.find_stuck_jobs(self.stuck_threshold).await;
for job in stuck_jobs {
if job.repair_attempts >= self.max_repair_attempts {
// 최대 시도 초과 → Failed로 전환 (알림 스팸 방지)
db.update_job_status(job.id, Status::Failed).await;
} else {
// InProgress로 복구 시도
db.update_job_status(job.id, Status::InProgress).await;
db.increment_repair_attempts(job.id).await;
}
}
// ── (B) 고장난 도구 감지 및 재빌드 ──
let broken_tools = db.find_broken_tools(5).await; // 5회+ 실패
for tool in broken_tools {
// 내장 도구는 재빌드 대상에서 제외
if is_protected_tool_name(&tool.name) {
continue;
}
// LLM을 사용하여 WASM 도구 자동 재빌드
let builder = SoftwareBuilder::new();
builder.rebuild(tool).await;
}
}
}
}
이것이 진짜 "에이전틱"한 부분
자기 수리 시스템은 에이전트 루프 바깥에서 에이전트를 감시하는 메타 루프다. 에이전트가 멈추면 복구하고, 도구가 고장나면 재빌드한다. 이는 단순한 "while + tool call" 루프를 넘어서, 시스템이 자기 자신을 유지보수하는 능력을 의미한다. Pi/Hermes/OpenClaw에는 이 메커니즘이 없다.
6. 3계층 메모리 아키텍처 MEMORY
6.1 계층 구조
┌────────────────────────────────────────────────────────────┐
│ Layer 3: 영구 메모리 (Persistent Memory) │
│ memory.rs / MemoryStore │
│ DB 기반. Summary, Lesson, Spec, Issue, Note, Plan, Skill │
│ FTS + 시맨틱 검색. 스레드 간 지식 전달. │
│ Spec(0.5) > Skill(0.45) > Lesson(0.4) > Plan(0.3) │
├────────────────────────────────────────────────────────────┤
│ Layer 2: 컨텍스트 압축 (Context Compaction) │
│ compaction.rs / default.py │
│ 3가지 전략: Summarize / Truncate / MoveToWorkspace │
│ V2: 85% 임계치, LLM 요약, 원본 스냅샷 보관 │
├────────────────────────────────────────────────────────────┤
│ Layer 1: 작업 메모리 (Working Memory) │
│ memory.rs / ConversationMemory │
│ 인메모리 대화 기록. max_messages=100 │
│ 시스템 메시지는 절대 제거 안 함. 작업(Job) 단위 격리. │
└────────────────────────────────────────────────────────────┘
6.2 문서 검색 + 컨텍스트 자동 주입
// V2 루프의 첫 스텝에서 자동 실행 (default.py, 756행)
# 목표(goal)에 기반한 관련 문서 검색
docs = __retrieve_docs__(goal, max_results=5)
# RetrievalEngine의 검색 로직:
# 1. 키워드 매칭으로 후보 필터링
# 2. 문서 타입별 가중치 적용
# Spec(0.5) > Skill(0.45) > Lesson(0.4) > Plan(0.3)
# > Issue(0.2) > Summary(0.1) > Note(0.05)
# 3. 상위 N개를 시스템 메시지로 주입
if docs:
context.append({
"role": "system",
"content": format_docs(docs)
})
Insight: 메모리의 핵심은 "검색"이지 "저장"이 아니다
Ironclaw의 메모리 시스템이 다른 시스템과 구별되는 점은 문서 타입별 가중치다. Spec(사양)이 Note(메모)보다 5배 중요하다는 것은 "이전에 배운 것(Lesson) > 지금 계획하는 것(Plan) > 현재 이슈(Issue)"라는 우선순위를 의미한다. 이 가중치가 검색 품질을 결정하고, 검색 품질이 에이전트의 판단 품질을 결정한다.
6.3 컨텍스트 모니터 — 사용률 기반 전략 선택
// src/agent/context_monitor.rs
pub fn recommend_strategy(usage_percent: f64) -> CompactionStrategy {
match usage_percent {
p if p > 95.0 => {
// 위험: 공격적 절단 (최근 3턴만 유지)
CompactionStrategy::Truncate { keep_recent: 3 }
}
p if p > 85.0 => {
// 경고: LLM 요약 (최근 5턴 유지)
CompactionStrategy::Summarize { keep_recent: 5 }
}
p if p > 80.0 => {
// 안전: 워크스페이스로 이동
CompactionStrategy::MoveToWorkspace
}
_ => CompactionStrategy::None
}
}
// 안전장치: 워크스페이스 쓰기 실패 시 턴을 절대 삭제하지 않음
if let Err(e) = workspace.write(turns_to_archive).await {
warn!("Workspace write failed: {}. NOT deleting turns.", e);
return; // 정보 손실 방지
}
7. 자기 수정 오케스트레이터 V2 ONLY
crates/ironclaw_engine/src/executor/orchestrator.rs// 오케스트레이터 로딩 로직
pub fn load_orchestrator(store: &MemoryStore) -> String {
// 자기 수정이 비활성화되어 있으면 컴파일된 v0 코드 사용
if !env::var("ORCHESTRATOR_SELF_MODIFY").is_ok() {
return default_orchestrator_v0();
}
// Store에서 최신 오케스트레이터 코드 로드
let latest = store.get_latest("orchestrator/default.py");
match latest {
Some(code) => {
// Python 구문 검증
if !validate_python_syntax(&code) {
warn!("Modified orchestrator has syntax errors, falling back");
return default_orchestrator_v0();
}
// 실패 카운터 확인
let failures = store.get_failure_count("orchestrator");
if failures >= MAX_FAILURES_BEFORE_ROLLBACK { // = 3
warn!("Orchestrator failed {} times, rolling back", failures);
return default_orchestrator_v0();
}
code
}
None => default_orchestrator_v0()
}
}
// 보호 장치: memory_write에서 오케스트레이터 경로 보호
fn is_protected_orchestrator_path(path: &str) -> bool {
let normalized = normalize_path(path); // dot-segment 우회 방지
normalized.starts_with("orchestrator/")
}
// 자기 수정 활성화 시 인간 승인 필수
fn requires_approval(&self, params: &WriteParams) -> ApprovalRequirement {
if is_protected_orchestrator_path(¶ms.path) {
ApprovalRequirement::Always // 항상 인간 승인 필요
} else {
ApprovalRequirement::UnlessAutoApproved
}
}
이것이 4개 시스템 중 가장 독창적인 설계
에이전트의 실행 루프 코드 자체가 수정 대상이 된다. "에이전트가 더 효율적인 실행 전략을 발견하면 자기 자신의 루프를 수정한다" — 이는 메타 학습(meta-learning)의 실용적 구현이다.보호 장치가 3중으로 설계된 이유: (1) 구문 검증으로 문법 오류 차단, (2) 3회 연속 실패 시 자동 롤백, (3) 인간 승인 필수. 이 없이는 에이전트가 자기 루프를 망가뜨리면 복구 불가능해진다.
8. 종합 인사이트
1. "무인 운영"을 위한 설계
Pi는 "개발자가 터미널 앞에 앉아있다"를 전제하고, ironclaw는 "아무도 보고 있지 않다"를 전제한다. 이 차이가 중복 감지, 자기 수리, 예산 시스템, 다중 종료 조건이라는 모든 추가 복잡성의 근원이다. 닫힌 루프를 만들려면 ironclaw의 방어 패턴을 참고해야 한다.
2. 이중 엔진은 점진적 진화 전략
V1(Rust, 고정)에서 V2(Python, 가변)로의 전환은 "안정성 vs 적응성" 스펙트럼에서의 의도적 이동이다. V1은 절대 망가지지 않지만 개선도 안 되고, V2는 자기 개선이 가능하지만 망가질 수도 있다. 3회 롤백 규칙이 이 균형을 잡는다.
3. 7가지 종료 조건 = 방어적 설계의 핵심
(1) 텍스트 응답, (2) FINAL() 호출, (3) 최대 반복, (4) 정지 신호, (5) 예산 소진, (6) 연속 에러, (7) 승인 대기. 어떤 상황에서도 루프가 영원히 돌지 않도록 보장한다. Pi의 "maxSteps 없음"과 정확히 반대되는 철학.
4. 40+ 내장 도구 — "에이전트 OS"
Ironclaw의 도구 목록(파일, 셸, 네트워크, 메모리, 작업 관리, 루틴, 이미지, 시스템, 확장, 스킬, 시크릿 등)은 단순한 코딩 에이전트를 넘어 운영 체제의 시스템 콜에 가깝다. 도구 빌더(SoftwareBuilder)로 새 WASM 도구를 자동 생성할 수 있다는 점이 이 비유를 완성한다.