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 Up to 40 iterations
L->>LLM: callLLMWithRetry(messages, tools)
LLM-->>L: { content, toolCalls, usage }
alt No tool calls
L->>L: Check background results
alt Has background results
L->>L: Inject background results into messages
L->>L: continue (not consuming iteration)
else No background results
L-->>S: AgentLoopResult (finishReason: stop)
end
else Has tool calls
L->>L: executeToolCalls(parallel)
Note over L,T: Security pipeline: 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: Append tool results to messages
L->>L: Drain background results
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 レスポンスには 3 つの部分が含まれます。
| フィールド | 説明 |
|---|---|
content | テキスト返信(<think> ブロックを含む場合があります) |
toolCalls | ツール呼び出しリクエストのリスト |
finishReason | stop / tool_use / max_tokens / error |
ツール呼び出しがない場合:
<think>...</think>ブロックを除去する(DeepSeek 推論形式)text_delta進捗イベントを送信する- 保留中のバックグラウンド sub-Agent 結果があるか確認する
- バックグラウンド結果が存在し、最大ドレインラウンド(3)を超えていない場合 → 結果を注入してループを続行
- それ以外の場合は最終結果を返す
ステップ 3: ツール実行(並列)
同じ LLM レスポンス内のすべてのツール呼び出しは 並列 に実行されます(Promise.all)。各ツール呼び出しは、次のセキュリティパイプラインを通過します。
graph LR
TC["Tool call request"] --> Abort{"Check Abort"}
Abort -->|Aborted| Err1["Return error"]
Abort -->|Not aborted| NS{"Native search skip?"}
NS -->|Yes| Skip["Skip (provider handles)"]
NS -->|No| Chan{"Channel role check"}
Chan -->|Denied| Err2["No permission"]
Chan -->|Allowed| Hook{"PreToolUse Hook"}
Hook -->|deny| Err3["Hook denied"]
Hook -->|allow| FW{"ExecutionFirewall"}
FW -->|blocked| Err4["Firewall denied"]
FW -->|allowed| GA{"GuardianAgent"}
GA -->|blocked| Err5["Guardian denied"]
GA -->|allowed| Perm{"Permission Callback"}
Perm -->|deny| Err6["User denied"]
Perm -->|allow| Exec["Execute tool<br/>5 minute timeout"]
Exec --> PostHook["PostToolUse Hook<br/>(fire-and-forget)"]
セキュリティパイプラインの詳細な手順:
- 中断チェック —
signal.abortedが true の場合、ただちに返す - ネイティブ検索のスキップ —
nativeSearchEnabledかつツールがネイティブ検索の場合、ローカル実行をスキップする - Channel ロールチェック —
channelUserPermissions.canUseTool === falseの場合、すべてのツールをブロックする - PreToolUse Hook — 登録済みフックを実行し、
behavior === 'deny'の場合はブロックする - ExecutionFirewall — ファイルパスとコマンドが拒否ゾーンに触れていないか確認する
- GuardianAgent — LLM によるセキュリティ評価(有効な場合)
- 権限コールバック — 機密性の高いツールはユーザー確認を必要とする
- 実行 — タイムアウト付きのツール実行(
Promise.race対タイムアウト Promise) - PostToolUse Hook — 非同期トリガー、非ブロッキング(fire-and-forget)
ステップ 4: 結果処理
- 出力の切り詰め —
TOOL_RESULT_MAX_CHARS(50KB)を超える結果は切り詰められる - YieldSignal チェック — ツールが
YieldSignalをスローした場合、ただちにループを終了する - イテレーション完了コールバック —
await onIterationComplete()がメッセージを DB に保存する - メッセージへ追加 — ツール結果を
role: 'tool'メッセージとして追加する - バックグラウンド結果のドレイン — 完了したバックグラウンド sub-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 バージョン |
バックグラウンド sub-Agent 連携
TinyElf はバックグラウンド sub-Agent の並列実行に対応しています。メインループは次の仕組みでバックグラウンドタスクと連携します。
結果注入
pushBackgroundResult(result: BackgroundAgentResult): void
バックグラウンド sub-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 は当初、Claude SDK とのクロスエンジン相互運用性のためにスキーマを共有する目的で、
TinyElfSdkAdapter.convertSingleMessage()により SDK JSONL スタイルへ変換し、各メッセージをsdk_recordsテーブルへ書き込んでいました。しかし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) - Hook 拡張 —
HookExecutor経由で PreToolUse / PostToolUse / SessionStart / SessionEnd フックを登録する
関連モジュール
| モジュール | パス | 関係 |
|---|---|---|
| ToolRegistryBuilder | tinyelf/TinyElfToolRegistryBuilder.ts | ツールレジストリを構築する |
| SubagentManager | tinyelf/tools/SpawnTool.ts | Sub-Agent 管理 |
| ExecutionFirewall | platform/security/ExecutionFirewall.ts | レイヤー 1 セキュリティ |
| GuardianAgent | platform/security/GuardianAgent.ts | レイヤー 2 セキュリティ |
| TinyElfPermissions | tinyelf/TinyElfPermissions.ts | レイヤー 3 セキュリティ |