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 | 도구 호출 요청 목록 |
finishReason | stop / tool_use / max_tokens / error |
도구 호출이 없는 경우:
<think>...</think>블록 제거 (DeepSeek 추론 형식)text_delta진행 이벤트 전송- 대기 중인 백그라운드 서브 Agent 결과 확인
- 백그라운드 결과가 있고 최대 드레인 횟수(3회)를 초과하지 않은 경우 → 결과 주입 후 루프 계속
- 그렇지 않으면 최종 결과 반환
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)"]
보안 파이프라인 상세 단계:
- Abort 확인 —
signal.aborted가 true이면 즉시 반환 - 네이티브 검색 스킵 —
nativeSearchEnabled이고 도구가 네이티브 검색인 경우 로컬 실행 건너뜀 - 채널 역할 확인 —
channelUserPermissions.canUseTool === false이면 모든 도구 차단 - PreToolUse Hook — 등록된 훅 실행,
behavior === 'deny'이면 차단 - ExecutionFirewall — 파일 경로 및 명령이 거부 구역에 접근하는지 확인
- GuardianAgent — LLM 보안 평가 (활성화된 경우)
- Permission Callback — 민감한 도구는 사용자 확인 필요
- 실행 — 타임아웃 적용 도구 실행 (
Promise.racevs 타임아웃 Promise) - PostToolUse Hook — 비동기 트리거, 비블로킹 (fire-and-forget)
4단계: 결과 처리
- 출력 잘라내기 —
TOOL_RESULT_MAX_CHARS(50KB)를 초과하는 결과는 잘라냄 - YieldSignal 확인 — 도구에서
YieldSignal을 던지면 즉시 루프 종료 - 반복 완료 콜백 —
await onIterationComplete()로 DB에 메시지 저장 - 메시지에 추가 — 도구 결과를
role: 'tool'메시지로 추가 - 백그라운드 결과 드레인 — 완료된 백그라운드 서브 Agent 결과를 메시지 목록에 주입
5단계: 루프 제어
다음 종료 조건 중 하나가 충족될 때까지 1단계로 돌아갑니다:
| 종료 조건 | finishReason |
|---|---|
| LLM이 일반 텍스트 반환 (도구 호출 없음) | stop |
| 최대 반복 횟수 도달 | max_iterations |
| AbortSignal 트리거됨 | interrupted |
| LLM 오류 반환 | error |
| YieldSignal 트리거됨 | stop |
상수 정의
| 상수 | 값 | 설명 |
|---|---|---|
MAX_ITERATIONS | 40 | 최대 루프 반복 횟수 |
TEMPERATURE | 0.1 | LLM 온도 파라미터 |
MAX_TOKENS | 8192 | LLM 최대 출력 토큰 수 |
TOOL_RESULT_MAX_CHARS | 50,000 | 도구 결과 최대 문자 수 |
MAX_LLM_RETRIES | 2 | LLM 호출 재시도 횟수 |
LLM_RETRY_DELAY_MS | 1,000 | 재시도 기본 지연 시간 (ms) |
DEFAULT_TOOL_EXECUTION_TIMEOUT_MS | 300,000 | 단일 도구 실행 타임아웃 (5분) |
PERMISSION_TIMEOUT | 300,000 | 권한 확인 타임아웃 (5분) |
MAX_BACKGROUND_DRAIN_ROUNDS | 3 | 최대 연속 백그라운드 드레인 횟수 |
DEFAULT_MAX_HISTORY | 100 | 로드되는 최대 히스토리 메시지 수 |
VERSION | 0.1.0 | TinyElf 버전 |
백그라운드 서브 Agent 통합
TinyElf는 백그라운드 서브 Agent의 병렬 실행을 지원합니다. 메인 루프는 다음 메커니즘을 통해 백그라운드 작업과 협력합니다:
결과 주입
pushBackgroundResult(result: BackgroundAgentResult): void
백그라운드 서브 Agent 완료 후, SubagentManager.setBackgroundCompleteCallback을 통해 결과를 메인 루프 큐에 푸시합니다.
드레인 전략
- 각 반복 종료 시 백그라운드 결과 큐 확인
- 새 결과가 있으면
[Background agent "label" (runId) outcome]형식으로 주입 - LLM이 일반 텍스트를 반환하지만 대기 중인 백그라운드 결과가 있을 경우 루프 계속 (최대 3회)
- 실제 도구 호출 시 드레인 카운터 초기화
메시지 영속성
각 반복 완료 시, onIterationComplete 콜백을 통해 메시지를 저장합니다:
MessageBlock[]빌드 (thinking + tool_use 블록)- 어시스턴트 메시지 저장 (도구 호출 정보 포함)
- 각 도구 결과 메시지 저장 (
tool_result블록 포함) - 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.ts | IEngine 구현 |
| 세션 러너 | tinyelf/TinyElfSessionRunner.ts | 세션 실행 오케스트레이션 |
| LLM 어댑터 | tinyelf/TinyElfLLMAdapter.ts | LLM 호출 어댑테이션 |
| 프롬프트 빌더 | 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 훅 등록
관련 모듈
| 모듈 | 경로 | 관계 |
|---|---|---|
| ToolRegistryBuilder | tinyelf/TinyElfToolRegistryBuilder.ts | 도구 레지스트리 빌드 |
| SubagentManager | tinyelf/tools/SpawnTool.ts | 서브 Agent 관리 |
| ExecutionFirewall | platform/security/ExecutionFirewall.ts | 1계층 보안 |
| GuardianAgent | platform/security/GuardianAgent.ts | 2계층 보안 |
| TinyElfPermissions | tinyelf/TinyElfPermissions.ts | 3계층 보안 |