본문으로 건너뛰기

TinyElf Agent 루프 심층 분석

TinyElf는 Elftia의 내장 Agent 엔진으로, "LLM 호출 → 도구 실행 → 결과 관찰 → 반복"이라는 고전적인 패턴을 구현합니다. 이 문서는 전체 Agent 루프 알고리즘을 상세히 분석합니다.

전체 아키텍처

graph TB
Engine["TinyElfEngine"] --> SessionRunner["TinyElfSessionRunner<br/>runSession()"]

SessionRunner --> ToolBuilder["TinyElfToolRegistryBuilder<br/>buildToolRegistry()"]
SessionRunner --> PromptBuilder["TinyElfPromptBuilder<br/>buildMessages()"]
SessionRunner --> LoopInst["TinyElfAgentLoop<br/>instantiation"]
SessionRunner --> |"run"| Loop["AgentLoop.run()"]

ToolBuilder --> FileTools["FileSystemTools"]
ToolBuilder --> ShellT["ShellTool"]
ToolBuilder --> WebT["WebSearch/WebFetch"]
ToolBuilder --> SkillsT["Skills tools"]
ToolBuilder --> SpawnT["SpawnTool"]
ToolBuilder --> McpT["MCP tools"]
ToolBuilder --> SessionT["Session tools"]

Loop --> LLMCall["callLLMWithRetry()"]
Loop --> ToolExec["executeToolCalls()"]
Loop --> Save["onIterationComplete()"]

세션 실행 흐름

sequenceDiagram
participant R as AgentRouter
participant E as TinyElfEngine
participant B as ToolRegistryBuilder
participant S as SessionRunner
participant L as AgentLoop
participant LLM as TinyElfLLMAdapter
participant T as ToolRegistry

R->>E: startSession(ctx)
E->>E: saveMessage(user)
E->>E: sender.send(userMessage)
E->>B: buildToolRegistry()
B-->>E: { toolRegistry, subagentManager, ... }
E->>S: runSession(deps, params)
S->>S: new TinyElfLLMAdapter()
S->>S: new ExecutionFirewall()
S->>S: buildGuardian()
S->>L: new TinyElfAgentLoop()
S->>S: buildMessages()
S->>L: run(messages, callbacks)

loop 최대 40회 반복
L->>LLM: callLLMWithRetry(messages, tools)
LLM-->>L: { content, toolCalls, usage }

alt 도구 호출 없음
L->>L: 백그라운드 결과 확인
alt 백그라운드 결과 있음
L->>L: 메시지에 백그라운드 결과 주입
L->>L: continue (반복 횟수 소모 안 함)
else 백그라운드 결과 없음
L-->>S: AgentLoopResult (finishReason: stop)
end
else 도구 호출 있음
L->>L: executeToolCalls(parallel)
Note over L,T: 보안 파이프라인: Firewall → Guardian → Permission
L->>T: execute(tool)
T-->>L: ToolCallResult
L->>S: onIterationComplete(data)
S->>S: saveMessage(assistant + tools)
S->>S: sender.send(assistantMessage)
L->>L: 도구 결과를 메시지에 추가
L->>L: 백그라운드 결과 드레인
end
end

S->>S: saveFinalResult()
S->>S: sender.send(result + complete)

Agent 루프 알고리즘

1단계: LLM 호출 (재시도 포함)

callLLMWithRetry(messages, tools, signal)
  • 최대 MAX_LLM_RETRIES (2)회 시도
  • 재시도 지연: LLM_RETRY_DELAY_MS * (attempt + 1) (1초, 2초, 3초 순으로 증가)
  • 재시도 조건: LLM이 콘텐츠 없이 finishReason === 'error'를 반환하거나 예외 발생 시
  • 모든 시도 실패 후 오류 메시지 반환
  • 각 재시도 전에 AbortSignal 확인

2단계: 응답 파싱

LLM 응답은 세 부분으로 구성됩니다:

필드설명
content텍스트 응답 (<think> 블록 포함 가능)
toolCalls도구 호출 요청 목록
finishReasonstop / tool_use / max_tokens / error

도구 호출이 없는 경우:

  1. <think>...</think> 블록 제거 (DeepSeek 추론 형식)
  2. text_delta 진행 이벤트 전송
  3. 대기 중인 백그라운드 서브 Agent 결과 확인
  4. 백그라운드 결과가 있고 최대 드레인 횟수(3회)를 초과하지 않은 경우 → 결과 주입 후 루프 계속
  5. 그렇지 않으면 최종 결과 반환

3단계: 도구 실행 (병렬)

동일한 LLM 응답 내의 모든 도구 호출은 병렬(Promise.all)로 실행됩니다. 각 도구 호출은 다음 보안 파이프라인을 통과합니다:

graph LR
TC["도구 호출 요청"] --> Abort{"Abort 확인"}
Abort -->|중단됨| Err1["오류 반환"]
Abort -->|중단 안 됨| NS{"네이티브 검색 스킵?"}
NS -->|예| Skip["스킵 (프로바이더가 처리)"]
NS -->|아니오| Chan{"채널 역할 확인"}
Chan -->|거부| Err2["권한 없음"]
Chan -->|허용| Hook{"PreToolUse Hook"}
Hook -->|deny| Err3["훅에 의해 거부"]
Hook -->|allow| FW{"ExecutionFirewall"}
FW -->|blocked| Err4["방화벽 거부"]
FW -->|allowed| GA{"GuardianAgent"}
GA -->|blocked| Err5["Guardian 거부"]
GA -->|allowed| Perm{"Permission Callback"}
Perm -->|deny| Err6["사용자 거부"]
Perm -->|allow| Exec["도구 실행<br/>5분 타임아웃"]
Exec --> PostHook["PostToolUse Hook<br/>(fire-and-forget)"]

보안 파이프라인 상세 단계:

  1. Abort 확인signal.aborted가 true이면 즉시 반환
  2. 네이티브 검색 스킵nativeSearchEnabled이고 도구가 네이티브 검색인 경우 로컬 실행 건너뜀
  3. 채널 역할 확인channelUserPermissions.canUseTool === false이면 모든 도구 차단
  4. PreToolUse Hook — 등록된 훅 실행, behavior === 'deny'이면 차단
  5. ExecutionFirewall — 파일 경로 및 명령이 거부 구역에 접근하는지 확인
  6. GuardianAgent — LLM 보안 평가 (활성화된 경우)
  7. Permission Callback — 민감한 도구는 사용자 확인 필요
  8. 실행 — 타임아웃 적용 도구 실행 (Promise.race vs 타임아웃 Promise)
  9. PostToolUse Hook — 비동기 트리거, 비블로킹 (fire-and-forget)

4단계: 결과 처리

  1. 출력 잘라내기TOOL_RESULT_MAX_CHARS (50KB)를 초과하는 결과는 잘라냄
  2. YieldSignal 확인 — 도구에서 YieldSignal을 던지면 즉시 루프 종료
  3. 반복 완료 콜백await onIterationComplete()로 DB에 메시지 저장
  4. 메시지에 추가 — 도구 결과를 role: 'tool' 메시지로 추가
  5. 백그라운드 결과 드레인 — 완료된 백그라운드 서브 Agent 결과를 메시지 목록에 주입

5단계: 루프 제어

다음 종료 조건 중 하나가 충족될 때까지 1단계로 돌아갑니다:

종료 조건finishReason
LLM이 일반 텍스트 반환 (도구 호출 없음)stop
최대 반복 횟수 도달max_iterations
AbortSignal 트리거됨interrupted
LLM 오류 반환error
YieldSignal 트리거됨stop

상수 정의

상수설명
MAX_ITERATIONS40최대 루프 반복 횟수
TEMPERATURE0.1LLM 온도 파라미터
MAX_TOKENS8192LLM 최대 출력 토큰 수
TOOL_RESULT_MAX_CHARS50,000도구 결과 최대 문자 수
MAX_LLM_RETRIES2LLM 호출 재시도 횟수
LLM_RETRY_DELAY_MS1,000재시도 기본 지연 시간 (ms)
DEFAULT_TOOL_EXECUTION_TIMEOUT_MS300,000단일 도구 실행 타임아웃 (5분)
PERMISSION_TIMEOUT300,000권한 확인 타임아웃 (5분)
MAX_BACKGROUND_DRAIN_ROUNDS3최대 연속 백그라운드 드레인 횟수
DEFAULT_MAX_HISTORY100로드되는 최대 히스토리 메시지 수
VERSION0.1.0TinyElf 버전

백그라운드 서브 Agent 통합

TinyElf는 백그라운드 서브 Agent의 병렬 실행을 지원합니다. 메인 루프는 다음 메커니즘을 통해 백그라운드 작업과 협력합니다:

결과 주입

pushBackgroundResult(result: BackgroundAgentResult): void

백그라운드 서브 Agent 완료 후, SubagentManager.setBackgroundCompleteCallback을 통해 결과를 메인 루프 큐에 푸시합니다.

드레인 전략

  • 각 반복 종료 시 백그라운드 결과 큐 확인
  • 새 결과가 있으면 [Background agent "label" (runId) outcome] 형식으로 주입
  • LLM이 일반 텍스트를 반환하지만 대기 중인 백그라운드 결과가 있을 경우 루프 계속 (최대 3회)
  • 실제 도구 호출 시 드레인 카운터 초기화

메시지 영속성

각 반복 완료 시, onIterationComplete 콜백을 통해 메시지를 저장합니다:

  1. MessageBlock[] 빌드 (thinking + tool_use 블록)
  2. 어시스턴트 메시지 저장 (도구 호출 정보 포함)
  3. 각 도구 결과 메시지 저장 (tool_result 블록 포함)
  4. IPC를 통해 assistantMessage 이벤트 전송

최종 일반 텍스트 응답은 saveFinalResult()를 통해 별도로 저장되며, 실행 통계(inputTokens, outputTokens)를 포함합니다.

SDK 이중 쓰기 (제거됨, 2026-05-19)

히스토리 노트: TinyElf는 원래 TinyElfSdkAdapter.convertSingleMessage()를 통해 각 메시지를 SDK JSONL 형식으로 변환하여 sdk_records 테이블에 기록했습니다. 이는 Claude SDK와 스키마를 공유하기 위한 크로스 엔진 상호운용성을 위한 것이었습니다. 그러나 전체 sdk_records 메커니즘(SDK 측 IPC 미러 + TinyElf 측 합성)은 2026-05-19에 종료되었습니다 — Claude SDK 측 IPC 스키마가 디스크 JSONL 스키마와 호환되지 않으며(docs/dev/66_sdk_records/01_schema_divergence_investigation.md 참조), SDK 0.3.x는 이제 공식 SessionStore 인터페이스를 제공합니다(packages/desktop/app/main/services/agent-core/agent/SqliteSessionStore.ts 참조). TinyElf는 이제 영속성을 위해 chat_messages만 사용하며, SDK 미러는 필요하지 않습니다.

sdkDualWrite 상태 필드, initSdkDualWrite / initSdkDualWriteForResume / writeSdkRecords 메서드, 그리고 TinyElfSdkAdapter.ts 파일이 모두 제거되었습니다.

주요 파일

파일경로설명
Agent 루프tinyelf/TinyElfAgentLoop.ts핵심 루프 로직
엔진tinyelf/TinyElfEngine.tsIEngine 구현
세션 러너tinyelf/TinyElfSessionRunner.ts세션 실행 오케스트레이션
LLM 어댑터tinyelf/TinyElfLLMAdapter.tsLLM 호출 어댑테이션
프롬프트 빌더tinyelf/TinyElfPromptBuilder.ts메시지 구성
타입tinyelf/types.ts타입 및 상수 정의

모든 경로는 packages/desktop/app/main/services/agent-core/engine/ 기준입니다.

확장 포인트

  • 커스텀 도구 타임아웃TinyElfEngineConfig.toolExecutionTimeout
  • 커스텀 반복 횟수 제한TinyElfEngineConfig.maxIterations 또는 maxTurns
  • 커스텀 온도TinyElfEngineConfig.temperature
  • 생각 수준TinyElfEngineConfig.thinkingLevel (none/low/medium/high)
  • 훅 확장HookExecutor를 통해 PreToolUse / PostToolUse / SessionStart / SessionEnd 훅 등록

관련 모듈

모듈경로관계
ToolRegistryBuildertinyelf/TinyElfToolRegistryBuilder.ts도구 레지스트리 빌드
SubagentManagertinyelf/tools/SpawnTool.ts서브 Agent 관리
ExecutionFirewallplatform/security/ExecutionFirewall.ts1계층 보안
GuardianAgentplatform/security/GuardianAgent.ts2계층 보안
TinyElfPermissionstinyelf/TinyElfPermissions.ts3계층 보안