Skip to content

Latest commit

 

History

107 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

go-llm-agent

Go 1.25製のCGOなし単一バイナリAIエージェントです。OpenAI、Anthropic、Google Gemini、Ollama、llama.cpp (llama-server) を同一CLIとHTTP APIから扱えます。LiteLLMのように複数のLLMプロバイダーを統一インターフェースで操作でき、その上に薄いエージェントループ (tool calling、会話履歴、内蔵ツール) を提供します。

主な特徴

  • 単一バイナリで配布できます。CGO不要で Linux、macOS、Windowsのamd64とarm64に対応します
  • 5プロバイダー (OpenAI、Anthropic、Google Gemini、Ollama、llama.cpp) を統一した抽象層として扱えます
  • ストリーミングとtool callingに対応します
  • 内蔵ツールはfs_read、fs_write、fs_edit、shell、http_fetch、search_files、web_search、web_fetchの8種類です (ほかにRAG用のnote_add / note_search、自動メモリ用のmemory_write / memory_read)
  • 対話REPL、ワンショットrun、OpenAI互換HTTP APIの3種類のインターフェースを提供します
  • pre-commitとCIでgofmt、go vet、staticcheck、golangci-lint、govulncheck、race・coverage付きGoテスト、release build、gitleaksを実行します

クイックスタート

cp .env.example .env
$EDITOR .env                 # APIキーなどの実値を入れてください

cp config.yaml.example config.yaml
$EDITOR config.yaml          # allow_pathsなどを必要に応じて編集

make build
./bin/agent chat --model gemini/gemini-2.5-pro

config.yaml は個人ローカル設定として .gitignore 対象です。リポジトリには安全寄りの config.yaml.example のみがコミットされます。Anthropic/Claude のキーは ANTHROPIC_API_KEY を基本名とし、CLI は互換名として CLAUDE_API_KEY も試します。設定例の完全な内容は config.yaml.example を参照してください。

セキュリティ

このエージェントはモデル出力に駆動されるため、以下の多層的なガードを設計しています。 詳細は config.yaml.example のコメントも参照してください。

デフォルトは readonly

agent.enabled_tools を空または未指定にすると、Registryはfs_read / search_files / http_fetchreadonly セットのみを有効にします(tool.DefaultReadonlyTools)。 fs_writeshell を有効にする場合は意図して明示列挙してください。

部分編集 (fs_edit)

fs_edit はファイル全文を書き直さずに一部だけを置き換えるツールです。old_string に完全一致する部分文字列を new_string へ置換します。replace_all を指定しないときは一致がちょうど 1 件のときだけ実行し、0 件または複数件のときはエラーを返します。

対象ファイルは同一プロセス内で直前に fs_read 済みでなければ拒否します。読まずに書き換えて既存内容を失う事故を防ぐためです。ファイル全体を新しい内容へ置き換えるときは fs_write を使います。

fs_edit はプロセス単位の既読レジストリに依存するため、複数リクエストが混在する agent serve では自動的に無効化されます。

サンドボックスとセンシティブパス

tool.NewSandboxWithDeny はシンボリックリンク解決済みのパスを allow_paths と照合し、.. による上位ディレクトリ参照を拒否します。さらに、以下のセンシティブなパターンは設定で外せない強制 deny として常に拒否されます。

.git.env.env.*.ssh.aws.gnupg.npmrc.netrc.pypircid_rsa*id_dsa*id_ecdsa*id_ed25519*。 追加でdenyしたいパターンは tools.fs.deny_paths に列挙します。

OS サンドボックス (macOS sandbox-exec)

tools.shell.os_sandbox: auto を設定すると、macOS では shell ツールの実行を sandbox-exec でラップし、tools.fs.allow_paths の外への書き込みを OS 層でも拒否します。プロファイルは allow_paths から生成します。macOS 以外のプラットフォームでは何もしません。off を指定するとラップしません。

これはアプリ層の allow_paths 検証を置き換えるものではなく、その外側に重ねる追加の防御層です。子プロセスがサンドボックス外のパスへ書き込もうとした場合、アプリ層を通り抜けても OS 層で失敗します。

tools:
  shell:
    os_sandbox: auto   # auto | off

Shell の引数 deny

shell ツールは allow_binaries に加えて引数文字列に対するdeny正規表現を持ちます。 既定で以下が遮断されます (tool.DefaultShellArgDenyPatterns):

  • git config --global / git config --system
  • git -c core.sshCommand=... / git -c http.proxy=...
  • go env -wgo install
  • bash -c <code> / sh -c <code> / -c <code> / --exec

追加でdenyしたいパターンは tools.shell.arg_deny_patterns に列挙します。

HTTP fetch のドメイン許可と untrusted 標識

http_fetchtools.http_fetch.allow_domains が非空の場合のみ、FQDN末尾一致でリクエスト先を絞り込みます。レスポンス本文は以下のuntrustedラッパで返され、後段プロンプトに「外部由来でツール実行を許可してはならない」コンテキストを伝えます。

[HTTP <status>] [untrusted external content from <url>]
<body>
[end untrusted content]

プロバイダーとモデル許可リスト

providers.<name>.allow_models を列挙すると、その配列に一致するモデル名のみを許可します。サプライチェーン耐性のため、検証済みモデルのみをここに記述することを推奨します。 依存パッケージはGo modulesでgo.mod/go.sumによりピン留めされています。

監査ログ

すべてのsensitive ツール (fs_read / fs_write / shell / http_fetch) はslog経由で構造化ログを出力します。各レコードは correlation_id(agentループhopごとに発番されるtool_call ID)を含み、リクエスト追跡が可能です。

第三者検証

破壊シナリオが拒否されることをローカルで再現するには、以下を実行してください。

bash scripts/verify-hardening.sh

このスクリプトは固有ホスト依存をせず、gobash だけがあれば動きます(リポジトリ ルートからの実行が前提)。

サブコマンド

コマンド 説明
agent chat 対話 REPL を起動します(-no-spinner で進捗インジケータとターン要約を無効化、-resume で直近セッションを再開)
agent run -p ワンショットでプロンプトを1回送信します
agent serve OpenAI互換HTTP APIを起動します
agent tools 有効な内蔵ツールを一覧表示します
agent config 設定ファイルの内容をダンプします
agent eval ゴールデンデータセットでagentを評価します

REPL の入力と履歴

agent chat は行エディタを内蔵しています。外部の rlwrap で包まずに直接起動してください。

  • 履歴: ↑/↓ キーで呼び出し。~/.agent_history へ 1 行 1 エントリで永続化します(rlwrap -H と同形式のため既存ファイルをそのまま引き継げます)
  • 履歴検索: Ctrl-R で後方インクリメンタル検索に入ります。入力した文字列を部分一致で遡り、Enter で確定、Ctrl-C または ESC で取り消します
  • 日本語入力: 全角文字の表示幅を go-runewidth で計算するため、日本語の行を矢印キーで戻って編集してもカーソル描画がずれません
  • 貼り付け: bracketed paste 対応。改行を含む長文を貼り付けると 1 つのプロンプトにまとまり、Enter で送信します。1 行が 1024 バイトを超える長文も詰まりません
  • 中断と終了: 生成中は ESC でそのターンを中断、Ctrl-C でセッションを終了します
  • /help: 利用できるスラッシュコマンドの一覧を表示します
  • /model [provider/name]: 引数なしで現在のモデルと allow_models に基づく候補を表示し、引数ありでセッション中のモデルを切り替えます
  • /compact: 会話履歴を手動で圧縮します(下記「会話履歴の圧縮 (compaction)」参照)
  • /cost: セッション累計の入出力トークン数と、pricing 設定があれば概算コストを表示します
  • /tools off | on: ツール定義をリクエストに含めるかをセッション中に切り替えます。翻訳・要約など純粋な対話は off が安定します(下記「注意: ツール定義が小型モデルの指示追従を壊す」参照)。/tool でも可
  • /clear: 会話履歴を破棄し、セッション累計トークンをリセットして新しいセッションファイルへ切り替えます。モデルの回答が変な書式や話題に固定化したときの復旧手段です
  • /quit/exit: セッションを終了します
  • / で始まる未知の入力はコマンド一覧を表示し、LLM へは送りません(タイプミスした行が質問として履歴に入り、以後の回答を汚染する事故を防ぐため)
./bin/agent chat -config /path/to/config.yaml

セッション記録と再開

agent chat の会話は 1 セッション 1 ファイルの JSONL として storage.chat_sessions_dir に記録されます。このキーが空のときは <sessions_dir>/chat を使います。ファイル名は開始時刻の UTC タイムスタンプです。

-resume を付けると同じディレクトリで最も新しいセッションを読み込み、その履歴を引き継いで再開します。対象が 1 件も無い場合は新規セッションとして起動します。

./bin/agent chat -resume

AGENTS.md 自動読み込み

agent.agents_md.global_dir(既定~/.go-llm-agent)のAGENTS.md、次にtools.fs.allow_paths配下にあるカレントディレクトリの最も浅い祖先からカレントディレクトリまでの各AGENTS.mdを、この順に連結してシステムプロンプト末尾へ付加します。後方(カレントディレクトリに近い側)ほど実質的に優先されます。1ファイルはmax_bytes、連結合計はmax_total_bytes(既定32KiB、ファイル本文の合計で信頼境界マーカー分は含まない)で打ち切ります。各ファイルは由来パス付きの信頼境界マーカーで囲み、上位の指示や安全上の制約を上書きしない参考情報として渡します。

行頭の@相対パスは記述ファイル基準で展開します(深さ4まで、コードフェンス内は対象外、探索ルート外やシンボリックリンクは拒否)。agent run / agent serveは対象外です。

自動メモリ

エージェントがセッションをまたいで覚えておく事実を、agent.memory.dir(既定~/.go-llm-agent/projects)配下のプロジェクト単位ディレクトリ<プロジェクトキー>/memory/へMarkdownで保存します。プロジェクトキーはgitリポジトリのルート(worktreeとsubmoduleは主リポジトリ。gitdir:の逆参照を検証し、偽造した.gitファイルでは解決しない)から導出し、git外ではカレントディレクトリを使います。キーは<ディレクトリ名>-<ルート絶対パスのSHA-256先頭32桁(128bit)>で、区切り文字の置換だけでは衝突するパスも区別します。

  • MEMORY.mdは索引で、起動時に先頭200行かつ24KiB(index_max_lines / index_max_bytes)だけをシステムプロンプトへ注入します。信頼境界マーカーで囲み、コードから導出できる情報やAGENTS.mdに書いてある情報は保存しないという保存方針も添えます
  • memory_write / memory_readツールでエージェントがトピックファイル(<name>.md)を読み書きします。書き込み先はメモリディレクトリ直下に限定し、..や絶対パス、シンボリックリンク、1MiB超は拒否します。enabled_toolsへ2つを列挙すると使えます
  • REPLでは/memoryで一覧と索引を、/memory <file>で本文を表示します。# <本文>と入力するとmemories.mdと索引へ即時追記し、LLMへは送りません
  • agent serveではメモリツールを無効化します。自動メモリはプロジェクト単位の1ストアで、bearer認証は呼び出し元の識別をツールへ渡さないため、複数クライアント間でメモリが共有されてしまうからです
  • agent.memory.enabled: falseで全機能を無効化します。ディレクトリ初期化に失敗した場合はエラーログを出して無効化し、起動は継続します

評価フレームワーク

agent eval --suite <dir> --report <path> でYAML形式のゴールデンケースを実行し、tool_recall / tool_precision / param_accuracy / phrase_recall を計算したJSONレポートを書き出します。1 件でも合格条件を満たさなければ exit code 1 で停止するため、CIのクオリティゲートに組み込めます。

id: refund_simple
input:
  system_prompt: "あなたは返金支援エージェントです"
  messages:
    - role: user
      content: "order_id A89268 のマグカップだけ返金してください"
expected:
  tool_calls:
    - tool: refund_item
      params:
        order_id: A89268
  phrases: ["返金処理を受け付けました"]
metrics:
  tool_recall_min: 1.0
  param_accuracy_min: 1.0
  phrase_recall_min: 0.5

E2Eスクリプトは tests/e2e/07-eval-suite.sh です。fixtures/eval_exerciseがLoadSuite / Score / WriteReportの動作を検証します。設計の詳細は docs/design/07-eval-framework.md を参照してください。

ローカルノート (RAG MVP)

note_addnote_search の2つの内蔵ツールでJSONLベースのローカルノートを操作できます。スコアは title=3 / tags=2 / body=1 の重みで計算し、上位 top_k 件を返します。ノートは storage.notes_path (空なら sessions_dir/notes.jsonl) に追記します。memory.NoteStore インターフェースを介すため、将来SQLite FTS5やベクターDBに差し替え可能です。

E2Eスクリプトは tests/e2e/11-rag-mvp.sh で、fixtures/rag_exerciseが2件のノートを保存して全文検索が機能することを確認します。設計の詳細は docs/design/11-rag-mvp.md を参照してください。

Web 検索と本文取得 (web_search / web_fetch)

外部の最新情報を「検索 → URL 選択 → 本文取得 → 回答」の順で扱うための2ツールです。どちらも agent.enabled_tools に明示した場合のみ有効になります。

  • web_search: DuckDuckGo HTML から検索結果 (タイトル・URL・抜粋) を上位 N 件抽出して JSON で返します。広告ブロックは除外します。
  • web_fetch: URL の本文をボイラープレート除去済み Markdown で返します。長い本文は start_index でページングできます。

最新・現在・今日・時点・ニュース・天気、またはWeb・ネットでの検索を明示する入力では、agentが最初のLLM呼び出し前に web_search を1回実行し、成功時は検索結果の1件を web_fetch で1回取得します。モデルのtool choice対応に依存しないため、1回の入力で本文を根拠に回答できます。「公式」または official を含む入力では、入力中の英数字語とhost名が一致するURLを優先します。agent.tool_choice.mode: none はこの自動実行も無効にします。ローカルファイル検索との混同を避けるため、検索してバージョン だけでは自動実行しません。

REPLは各ターンのassistant tool callとtool結果を次のターンへ引き継ぎます。標準の prompts/coding-accuracy.md は、「もう少し詳しく」などの追質問に対して保存済み本文を使い、既出回答以外の背景・変更点・注意点・理由・具体例を2項目以上答える方針を定義します。agentは追質問の生成候補を画面へ送る前に検査し、既出内容、導入句だけの応答、prompt断片、Web本文に対応しない内容を除外します。本文を伴う2項目を満たさない場合は最大3回生成し直します。

web_fetch は外部 CLI webgrab に本文抽出を委譲します。webgrab は本文抽出 (Readability)、全リダイレクトホップでの SSRF 防止、出力インジェクション無害化を実装済みです。crates.io には未公開のため、webgrab のソースツリー (リポジトリ名 llm-web-fetch) を取得してソースからインストールします:

cd /path/to/llm-web-fetch
cargo install --path .

webgrab が PATH に無い環境では web_fetch は呼び出し時にエラーを返します (他のツールは影響を受けません)。

tools:
  web_search:
    max_results: 5
  web_fetch:
    webgrab_path: webgrab
    max_chars: 4000   # ctx 8192 のローカル LLM では 4000 前後を推奨
agent:
  enabled_tools: [fs_read, search_files, note_add, note_search, web_search, web_fetch]

取得内容は未検証の外部データとして [UNTRUSTED INPUT] 標識付きで LLM に渡ります。Web 由来の本文はプロンプトインジェクションのベクタになるため、fs_writeshell と併用する場合は agent.approval.required_tools に書き込み系ツールを列挙して HITL 承認を必ず挟んでください。取得結果を保存したい場合は LLM に note_add を使わせると RAG (ローカルノート) に蓄積されます。

E2Eスクリプトは tests/e2e/17-web-tools.sh で、実ネットワークに出ずにhttptestとスタブwebgrabを使い、各ツールとagentの検索→本文取得を検証します。設計の詳細は docs/design/17-web-search-fetch.md を参照してください。

コンテキスト拡充 (enricher)

ユーザーメッセージからプログラミング言語をキーワードベースで自動検出し、言語仕様リファレンスをLLM呼び出し前のコンテキストに注入してコーディング質問の回答精度を高めます。agent.WithContextEnricher オプションで注入され、システムプロンプト挿入後・入力スキャナ前に実行されます。enricherの失敗はwarnログを残して非拡充で続行するため、可用性に影響しません。

2つのモードがあり、dynamic を優先して結果が空なら languages の静的specファイルにフォールバックします。

  • 静的モード: languages に言語名とspecファイル (config.yamlからの相対パス) を列挙し、検出言語のファイル全文を注入します。
  • 動的モード: dynamic.sources の公式ドキュメントURL群をフェッチし、HTML→テキスト変換と見出し単位のセクション分割を行い、質問から抽出した識別子トークン (deferProc.new 等) とのキーワードマッチで上位 max_sections 件 (合計 max_bytes 以内) だけを注入します。フェッチ結果は cache_dir (既定はOSキャッシュディレクトリ) に cache_ttl_hours の間キャッシュされます。
agent:
  enricher:
    enabled: true
    prompts_dir: prompts
    languages:
      ruby: ruby-spec.md
      go: go-spec.md
    dynamic:
      enabled: true
      max_sections: 5
      max_bytes: 6000
      cache_ttl_hours: 24
      sources:
        go:
          - https://go.dev/ref/spec
          - https://go.dev/doc/faq

検出対応言語は ruby / go / python / javascript / typescript / react / csharp / java / springboot / rust です (検出キーワードは internal/enricher/enricher.go を参照)。ローカルLLMでの実測では、質問に関連するセクションだけを絞り込んで注入することが重要で、無関係な情報を含む大きなリファレンスの全文注入はかえって正答率を下げます。

注入効果はモデル依存です。実測 (3モデル x 6問のコーディング質問) では、コーディング特化モデル (qwen2.5-coder) はツール定義送信の抑制と決定的出力の併用で3.5点から5.5点 (6点満点) まで一貫して改善した一方、小型汎用モデル (gemma4) は素のまま (enricher無効) が最も安定しました。enricherはopt-inであり、無効時のリクエストは機能追加前とバイト同等です (素のgemma4で応答のバイト単位一致を実測確認済み)。ベンチマーク資材は scripts/bench_enricher.shbench-config.yaml / bench-config-plain.yaml を参照してください。

プロンプトテンプレート版管理

internal/prompt パッケージで <name>@<version>.tmpl 形式のテンプレートをファイルからロードできます。Renderertext/template の安全なサブセットを使い、許可リスト外の変数キーや欠落キーはrenderエラーとして弾きます。OTel spanの prompt.version 属性に乗せるとA/B比較ができます。

E2Eスクリプトは tests/e2e/13-prompt-template.sh で TestLoader_* と TestRenderer_* を -race付きで実行します。設計の詳細は docs/design/13-prompt-template-versioning.md を参照してください。

MCP クライアント

internal/mcp.Client で Model Context Protocol のstdio JSON-RPCサーバに接続し、tools/list でメソッドを発見し tools/call で実行できます。SSE transportは今後の拡張点で、現状はstdioのみサポートします。

c, err := mcp.NewStdioClient(ctx, []string{"./mcp/docs_server"})
tools, _ := c.ListTools(ctx)
res, _ := c.Call(ctx, "search_docs", json.RawMessage(`{"query":"x"}`))

E2Eスクリプトは tests/e2e/12-mcp-discovery.sh です。tests/e2e/fixtures/mcp_echo_server を子プロセスで起動して JSON-RPCハンドシェイクを確認します。設計の詳細は docs/design/12-mcp-client.md を参照してください。

並列ツール実行

service.ExecuteToolsParallel で複数ToolCallをsemaphore付きで並列実行できます。require_approval 対象のツールが1件でも含まれる場合は自動的に直列化 (バリア方式) し、人間オペレータの状況把握を優先します。fail_fast=true のときは最初の失敗で他の実行をキャンセルします。

agent:
  parallel_tools:
    enabled: true
    max_concurrency: 4
    fail_fast: false

E2Eスクリプトは tests/e2e/10-parallel-tools.sh で、go test -race 付きで TestExecuteToolsParallel_* を実行し、ゴルーチン競合や順序ずれが無いことを観測します。設計の詳細は docs/design/10-parallel-tool-calls.md を参照してください。

実行戦略の切替

agent.strategy で実行戦略を選べます。react (既定) はツール呼び出しの標準ループで、planner_executor はシステムプロンプトに計画指示を注入して executor_modelでツール呼び出しを行い、reflection は self-checkヒントを差し込みます。

agent:
  strategy: planner_executor
  planner_executor:
    planner_model: openai/gpt-4o
    executor_model: openai/gpt-4o-mini
    max_steps: 8
  reflection:
    max_iterations: 3
    trigger_consecutive_failures: 2
    trigger_hop_budget: 6

E2Eスクリプトは tests/e2e/09-planner-executor.sh です。fixtures/strategy_exerciseが3戦略とunknown値のフォールバック動作を検証します。設計の詳細は docs/design/09-planner-executor.md を参照してください。

HITL ツール承認

agent.approval.required_tools に含まれるツールは、実行前に承認が必要になります。承認の求め方はサブコマンドごとに自動で決まり、切り替えるための専用キーはありません。

  • agent chat では対話プロンプトを表示し、y で実行、n で拒否します。fs_writefs_edit の承認プロンプトには、適用前の unified diff を表示します。
  • agent serve では既存の broker を使います。/v1/runs/<runID>/approve にJSONで {call_id, allowed, reason, reviewer} をPOSTすると該当の承認待ちが解放されます。

timeout_seconds を過ぎた承認待ちは常に拒否されます。default_decisiondeny のみを受け付け、それ以外の値は起動時にエラーになります。fail-open になる設定はありません。

agent:
  approval:
    required_tools: [shell, fs_write, fs_edit]
    timeout_seconds: 30
    default_decision: deny

E2Eスクリプトは tests/e2e/08-hitl-approval.shtests/e2e/22-approval.sh です。fixtures / approval_exercise がRequest / Submit / timeoutの挙動を確認します。設計の詳細は docs/design/08-hitl-approval.md を参照してください。

hooks (pre_tool_use / post_tool_use)

hooks.pre_tool_usehooks.post_tool_use で、ツール実行の前後に外部コマンドを起動できます。契約は Claude Code の hooks と同じです。

  • matcher に一致したツールのときだけ起動します。* はすべてのツールに一致します。
  • stdin に JSON を渡します。pre は {"tool","args"}、post は {"tool","args","result":{"is_error","content","duration_ms"}} です。
  • exit 0 で許可、exit 2 で拒否します。拒否のときは stderr の内容を拒否理由として扱います。
  • timeout_seconds の既定は 10 秒です。タイムアウトやコマンド起動の失敗はツール実行を止めません。
  • hook command は sh -c で実行します。LinuxとmacOSではタイムアウト時にprocess group全体を停止します。その他のOSでは直接のhook processを停止して待機時間を制限し、子processの停止はOSの挙動に従います。
hooks:
  pre_tool_use:
    - matcher: shell
      command: ./scripts/audit-hook.sh
      timeout_seconds: 10
  post_tool_use: []

E2Eスクリプトは tests/e2e/26-hooks.sh で、pre hook の deny と allow、タイムアウト時の続行を検証します。設計の詳細は docs/specs/2026-08-13-improvements/09-hooks.md を参照してください。

PII 出力リダクション

safety.pii_redactor 設定でメール、日本語電話番号、マイナンバー、IPv4アドレスなどの個人情報パターンをagent出力のすべての経路でマスキングします。06番のOutputRedactorとChainRedactorで合成され、DeltaText / Final / ツール返却 / session 保存 / OTel span 属性のいずれにも同じマスク後文字列が乗ります。

E2Eスクリプトは tests/e2e/14-pii-redact.sh でTestPIIRedactor_* と TestChainRedactor_* を -race付きで実行します。設計の詳細は docs/design/14-pii-redaction.md を参照してください。

プロンプトインジェクション検知と出力リダクション

safety.input_scanner で入力テキストに対する正規表現スキャン、safety.output_redactor で出力テキストに対する機微情報マスキングを行います。すべてのツール返却テキストは [UNTRUSTED INPUT: tool=<name>] で始まるuntrustedマーカーで包まれ、LLMにuntrustedソースであることを明示します。

safety:
  input_scanner:
    enabled: true
    block_on_match: false
    patterns:
      - id: ignore_previous
        regex: "(?i)ignore (the )?previous instructions"
  output_redactor:
    enabled: true
    rules:
      - id: openai_key
        regex: "sk-[A-Za-z0-9]{20,}"
        replacement: "[REDACTED:OPENAI]"

DeltaText / Final / ツール出力のすべての経路で同じRedactorが適用されます。PII Redactorは ChainRedactor で本Redactorの後段に組み合わされます。

E2Eスクリプトは tests/e2e/06-injection-and-redact.sh です。fixtures/safety_exerciseがScannerとRedactorの動作を検証します。設計の詳細は docs/design/06-input-output-filter.md を参照してください。

ツール呼び出しの強制度とスキーマ検証

agent.tool_choice でLLM のツール呼び出し挙動を制御できます。modeauto / required / none / tool の 4 種類で、tool を指定したときは name に具体的なツール名を入れます。各プロバイダー (OpenAI / Anthropic / Gemini / Ollama / llama.cpp) のネイティブなtool_choice仕様にマッピングされます。

mode: none はツール定義の送信ごと抑制します。定義を送ったまま「呼ぶな」と指示するだけでは、tool_choiceを無視するモデルがツール呼び出しJSONをテキストとして出力する事故を防げないためです。純粋なQA用途では enabled_tools: [] がDefaultReadonlyToolsにフォールバックする点に注意し、mode: none を明示してください。

agent.tool_validation で、ツール呼び出し時にLLMが生成するJSON引数を tool.Spec.Schema に照らして検証できます。スキーマ違反のときは max_retries 回までLLMに修正を促し、超過すると EventError で停止します。

agent:
  tool_choice:
    mode: auto
    name: ""
  tool_validation:
    enabled: true
    max_retries: 2

E2Eスクリプトは tests/e2e/05-tool-choice-validation.sh です。fixtures/tool_choice_exercise のOpenAI互換fakeサーバが受信するペイロードに tool_choice: required が正しくマッピングされていることを確認します。設計の詳細は docs/design/05-tool-choice-schema-validation.md を参照してください。

カナリアとシャドウデプロイ

internal/agent.Router でリクエスト単位のcanary振り分けとshadow実行設定を表現できます。Pick(seed) は決定論的で、同じseedには常に同じDecisionを返します。shadow ratioは副作用拡大を抑えるため0.5を上限としてハードキャップします。

r := agent.NewRouter("openai/gpt-4o-mini", "anthropic/claude-sonnet-4-6", 0.05, "openai/gpt-4o", 0.10)
d := r.Pick(seedFromRequest)

E2Eスクリプトは tests/e2e/16-canary-shadow.sh で TestRouter_* を -race付きで実行し、ratio=0/1の境界値と0.5 cap、同一seedの決定論性を確認します。設計の詳細は docs/design/16-canary-shadow.md を参照してください。

mTLS と OAuth2

internal/transport/httpapi.BuildTLSConfig でTLS終端とmTLSを構築できます。ClientCAFile を指定すると RequireAndVerifyClientCert でmTLSを強制します。MinVersion でTLSの最低バージョンも指定できます。

JWTVerifier はOAuth2リソースサーバの最小スタブです。MVPとしてshared_secret_env によるHS256検証だけ用意しており、JWKS fetchとRS256 / ES256検証は後続フェーズのgo-jwt統合で拡張します。

E2Eスクリプトは tests/e2e/15-mtls.sh でBuildTLSConfigとNewJWTVerifierの各分岐を -race付きで実行します。設計の詳細は docs/design/15-mtls-oauth.md を参照してください。

HTTP API の認証とレート制限

agent serve のHTTP APIは既定で無認証です。本番運用や127.0.0.1以外で待ち受ける場合は、Bearer Token認証、レート制限、IP allowlist、CORSをまとめて有効化できます。

server:
  addr: 0.0.0.0:14000
  auth:
    enabled: true
    bearer_tokens:
      - id: local
        secret_env: AGENT_LOCAL_TOKEN
  rate_limit:
    enabled: true
    rps: 5
    burst: 10
    per_token: true
  allowlist:
    cidrs: [127.0.0.1/32]
  cors:
    enabled: true
    allow_origins: [https://example.com]
    allow_methods: [GET, POST, OPTIONS]
    allow_headers: [Authorization, Content-Type]

トークン値は secret_env 経由でのみ与えられ、値の直書きは設定読込時に拒否されます。Authorization: Bearer <value><value>eyJ で始まるときは将来のOAuth2 JWT検証ミドルウェアに委譲する想定で素通しします。/healthz だけは認証とレート制限を回避します。

E2Eスクリプトは tests/e2e/04-http-auth.sh です。401 / 200 / 429 のすべてのシナリオをローカル環境変数のみで検証します。設計の詳細は docs/design/04-http-auth-ratelimit.md を参照してください。

リトライとフォールバック

providers.<name>.retry でリトライ設定を、fallback_to で別プロバイダーへの切替を指定できます。request_timeout_seconds は HTTPクライアント全体のタイムアウトを上書きします。

providers:
  openai:
    request_timeout_seconds: 60
    retry:
      max_attempts: 4
      initial_backoff_ms: 200
      max_backoff_ms: 5000
      jitter_ratio: 0.2
    fallback_to: anthropic

リトライ対象は llm.ProviderError.Retryable=true の429と5xx系のみで、4xxの入力エラーやcontextのキャンセルは即座に失敗します。バックオフは指数増加でジッタを掛け、MaxBackoff を上限とします。リトライ試行数とフォールバック発火回数はOTel メトリクス llm.retry.attemptsllm.fallback.total に記録されます。

E2Eスクリプトは tests/e2e/03-llm-retry.sh です。tests/e2e/fixtures/retry_exercise のフェイクプロバイダーを介して 429を2回返した後成功するシナリオを検証します。設計の詳細は docs/design/03-llm-retry-backoff.md を参照してください。

推論オプション (temperature / think)

providers.ollamatemperaturethink で推論パラメータを制御できます。どちらもポインタ型で、未指定なら一切リクエストに含めず Ollama の既定値に従います。

providers:
  ollama:
    base_url: http://localhost:11434
    temperature: 0   # 決定的出力 (ベンチマーク再現性)
    think: false     # thinking モード無効化 (Qwen 系 reasoning モデルの空応答・低速回避)

注意点として、効果はモデル依存です。実測では temperature: 0 はコーディング特化モデル (qwen2.5-coder) では精度と再現性を両立しましたが、小型汎用モデル (gemma4) では誤答の固定化、reasoningモデル (qwen3.5) ではgreedy decodingによる繰り返しループを誘発しました。think: false はQwen系reasoningモデルのQA用途で空応答 (thinkingがトークンを使い切る) を防ぐために実質必須です。

llama.cpp (llama-server) ローカル推論

providers.llamacpp は llama.cpp の llama-server (OpenAI 互換 API) に直結するプロバイダーです。Ollama デーモンを介さず GGUF を直接ロードして完全ローカルで tool calling まで動きます。llama-server は tool calling に --jinja が必須です。

providers:
  llamacpp:
    base_url: http://localhost:8080/v1
    request_timeout_seconds: 300
    think: false                    # Qwen 系の thinking 抑制 (chat_template_kwargs.enable_thinking=false)
    tool_call_id_format: "alnum9"   # Mistral-Nemo 系テンプレート専用の tool_call_id 対策
    allow_models: []

起動例:

llama-server -m model.gguf --jinja -c 8192 --port 8080

このプロバイダーは全リクエストに cache_prompt: true を送り、エージェントループの prefill 再利用を効かせます。ストリーミングで断片化して届く tool call は index 単位で連結し、各断片の arguments (JSON 文字列断片) をデコード結合して、完全なツール呼び出しとして 1 回だけ surface します。連結後の arguments はオブジェクト形式に正規化され、内蔵ツールがそのまま json.Unmarshal できます。

  • think はポインタ型で、false のとき chat_template_kwargs.enable_thinking=false を送信し Qwen 系 reasoning モデルの thinking を抑制します。未指定なら送信しません。Mistral 系など非 reasoning モデルでは不要です。
  • tool_call_id_format: "alnum9" は tool_call_id を 9 文字英数字へ決定的に書き換えます。Mistral-Nemo 系 (Shisa 等) のチャットテンプレートは tool_call_id に「9 文字英数字」を強制し、llama-server が生成する 32 文字 ID を 2 ターン目で拒否するため、その回避に使います。Qwen 系などこの制約を持たないモデルでは未指定にします。

常用する場合は、チャットテンプレートが tool calling をネイティブサポートする Gemma 4 12B 系 (実測構成は igorls/gemma-4-12B-it-heretic の Q8_0) を第一候補として推奨します。tool_call_id の形式制約が無いため tool_call_id_format は不要で、thinking は llama-server 側の --reasoning off で抑制できます。冗長な回答をしがちな傾向は、システムプロンプトに回答スタイルの制約を書くと抑えられます。実測ではツール往復、複数ターンの指示追従、15K トークン履歴でのセッション継続を確認済みです。Qwen 系 (think: false) も tool_call_id 制約が無く動きますが、Mistral-Nemo 系 (Shisa 等) は後述の指示追従問題があるため tool calling との併用には向きません。

注意: ツール定義が小型モデルの指示追従を壊す

小型の量子化モデルでは、リクエストにツール定義が含まれると、会話履歴があるときに長文プロンプトの指示を無視して直前ターンの話題に引きずられることがあります。「2 問目の回答が 1 問目の回答になる」「長文を貼り付けたのに前の話題を答える」という症状はこれです。

実測 (Shisa v2 12B i1-Q5_K_M、履歴 1 往復 + 約 1.5KB の翻訳依頼、temperature 0.2、各条件 3 回):

条件 結果
ツール定義 9 個 + 履歴 3/3 失敗 (前ターンの話題を回答)
ツール定義 5 個 + 履歴 3/3 失敗
ツール定義 2 個 + 履歴 2/3 失敗 (引きずりは消えるが指示を無視)
ツール定義なし + 履歴 3/3 成功

エージェントの送信内容は正常で (捕捉リクエストの byte 再生で確認)、原因はモデル側です。Mistral 系チャットテンプレートはツール定義 JSON を最後の user メッセージ直前に差し込むため、小型モデルが本来の指示を見失います。temperature を下げる (0.2) と揺らぎは減りますが、この症状自体は解消しません。

対処の選択肢:

  • REPL で /tools off を打つ (そのセッションのツール定義送信を止める。翻訳・要約が終わったら /tools on で戻す)
  • ツールを使わない対話が主なら agent.tool_choice.mode: none を設定する (ツール定義の送信ごと抑制され、指示追従はツールなし相当に安定する)
  • agent.enabled_tools を本当に使うものだけへ絞る (部分的な緩和)
  • tool calling を多用するなら、より大きい・ツール学習が強いモデルを検討する

macOS での常駐化 (launchd)

llama-server を launchd の LaunchAgent として登録すると、ログイン時に自動起動し、クラッシュ時も自動復帰します。

常駐化の前にリソース影響を実測してください。ps の RSS は mmap されたモデル重みを含まないため、footprint <pid> で dirty メモリを確認します。目安として 12B Q4 + -c 8192 の場合、常駐 dirty は約 3.7GB (大半が KV cache)、アイドル CPU は 1% 未満です。モデル重み本体は mmap のクリーンページとして扱われ、他のアプリがメモリを要求すると OS が自動回収するため、常駐しても開発作業を圧迫しません。dirty を減らしたい場合は -c を下げます (KV cache は -c にほぼ比例)。

~/Library/LaunchAgents/local.llama-server.plist を作成します:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>Label</key>
	<string>local.llama-server</string>
	<key>ProgramArguments</key>
	<array>
		<string>/opt/homebrew/bin/llama-server</string>
		<string>-m</string>
		<string>/path/to/model.gguf</string>
		<string>--jinja</string>
		<string>-c</string>
		<string>8192</string>
		<string>--port</string>
		<string>8080</string>
		<string>--host</string>
		<string>127.0.0.1</string>
	</array>
	<key>RunAtLoad</key>
	<true/>
	<key>KeepAlive</key>
	<dict>
		<key>SuccessfulExit</key>
		<false/>
	</dict>
	<key>ThrottleInterval</key>
	<integer>30</integer>
	<key>StandardOutPath</key>
	<string>/tmp/llama-server.log</string>
	<key>StandardErrorPath</key>
	<string>/tmp/llama-server.log</string>
	<key>ProcessType</key>
	<string>Background</string>
</dict>
</plist>

登録・停止・再開:

# 登録 (即起動)
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/local.llama-server.plist
# 疎通確認
curl -s http://127.0.0.1:8080/health
# 一時停止 (メモリを空けたいとき)
launchctl bootout gui/$(id -u)/local.llama-server
# 再開
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/local.llama-server.plist

登録後は手動で llama-server を起動しないでください (ポートが衝突します)。

会話履歴の圧縮 (compaction)

長い対話でコンテキスト長を超えないように、古い会話を要約へ置き換えます。直近ターンの実測 input tokens が context_window_tokens × trigger_ratio 以上になると自動で発火します。判定には推定値ではなく LLM 呼び出しが返した実測値を使います。

keep_recent_turns 件の直近ターンとシステムメッセージは保持し、それより前の区間を同じモデルで要約した 1 件のメッセージへ置き換えます。REPL で /compact を打つと同じ処理を手動で実行できます。圧縮対象が無い場合は何もしません。

agent:
  compaction:
    enabled: true
    context_window_tokens: 32768
    trigger_ratio: 0.7
    keep_recent_turns: 4

E2Eスクリプトは tests/e2e/19-compaction.sh です。設計の詳細は docs/specs/2026-08-13-improvements/01-compaction.md を参照してください。

ツール結果の切詰め

agent.tool_result_limit.max_chars を超えるツール出力は、会話履歴へ積む直前に切り詰めます。先頭 60% と末尾 40% を残し、中央を …[truncated: N chars omitted]… に置き換えます。末尾に出る終了コードやエラーメッセージを残すための方式です。

max_chars: -1 で切り詰めを無効化できます。0 は未指定と区別できないため、既定値が適用されます。既定値を変える場合は、ツールを 1 回使っただけで圧縮の閾値に達しないよう、context_window_tokens × trigger_ratio の 40% 以下に保ってください。

agent:
  tool_result_limit:
    max_chars: 8000

E2Eスクリプトは tests/e2e/20-truncation.sh です。設計の詳細は docs/specs/2026-08-13-improvements/02-truncation.md を参照してください。

トークンとコストの集計

providers.<name>.pricing を設定すると、LLM呼び出しごとの入出力トークン数からJPYコストを算出し、セッション単位と日次単位で集計します。集計結果は storage.sessions_dir/billing.jsonl に追記され、agent.EventUsageCost フィールド経由でリアルタイムに観測できます。

providers:
  openai:
    pricing:
      input_per_million_jpy: 450
      output_per_million_jpy: 1800
agent:
  budget:
    session_max_tokens: 200000
    daily_max_cost_jpy: 1000

予算上限を超える呼び出しは billing.ErrBudgetExceeded で停止します。0は無制限の指定です。

HTTP API は /v1/usage エンドポイントを提供します。?session=<id> でセッション単位、?date=YYYY-MM-DD(UTC)で日次の集計をJSONで返します。

curl http://127.0.0.1:14000/v1/usage?session=sess-1
curl http://127.0.0.1:14000/v1/usage?date=2026-05-23

E2Eスクリプトは tests/e2e/02-token-budget.sh です。LLMへの実通信は不要で、ローカルPC固有のAPIキーや課金には依存しません。設計の詳細は docs/design/02-token-cost-tracking.md を参照してください。

オブザーバビリティ

config.yamlobservability.otel セクションでOpenTelemetryのOTLP HTTP exporterを有効化できます。既定は無効で、有効化しても他機能の挙動は変わりません。

observability:
  otel:
    enabled: true
    exporter: otlp_http
    endpoint: 127.0.0.1:4318
    insecure: true
    sample_ratio: 1.0
    service_name: go-llm-agent
    metrics_interval_seconds: 30

エクスポート対象は次のとおりです。

  • スパン: agent.runllm.calltool.execute。親子関係がtrace上で1本につながります。
  • メトリクス: llm.tokens.inputllm.tokens.outputtool.duration_mstool.successtool.failurellm.retry.attemptsllm.fallback.total
  • ログ: obs.NewLogger でラップした slog レコードに trace_idspan_id の属性が付きます。

実動作確認はリポジトリ同梱のE2Eスクリプトで再現できます。Goとbashのみで動き、ローカルPC固有の設定には依存しません。

bash tests/e2e/01-otel-trace.sh

このスクリプトは tests/e2e/fixtures/otel_collector でOTLP HTTPの /v1/traces/v1/metrics を受け取るだけのモックを起動し、agent run の実行でモックにトレースとメトリクスが届くことを確認します。設計の詳細は docs/design/01-otel-instrumentation.md を参照してください。

開発者向け

make precommit-install   # pre-commit フックを有効化
make quality             # 品質ゲートをローカル実行(CI と同一フロー)
RUN_E2E=1 make quality   # 品質ゲートに28本のE2Eスクリプトを追加
make build-all           # 6 バイナリへクロスコンパイル

バージョン付与

バージョンはvMAJOR.MINOR.PATCHのsemantic versionで、mainへのマージごとにGitHub Actions(.github/workflows/release.yml)が自動で付与します。直近のvタグ以降のコミットをConventional Commitsとして読み、featならMINOR、fix/refactor/docs/choreなどならPATCH、type!:または本文のBREAKING CHANGEならMAJOR(0.x系の間はMINOR)を上げてタグとGitHub Release(6バイナリ添付)を作ります。計算規則はscripts/release/next-version.shが単一の情報源で、bash scripts/release/next-version_test.shで検証できます。起点は「マージ済みPRの本数」をMINORに置いたv0.13.0です。

ビルド時のバージョンは-ldflags -X main.version=...で埋め込み、agent versionで表示します。make buildは未指定ならgit describe --tagsの値(例: v0.13.0-3-gabc1234-dirty)を使います。

scripts/quality-gate.sh はpre-commitとCIが共有する品質確認の入口です。mutation対象packageの除外テスト、gofmt、go vet、staticcheck、golangci-lint、govulncheck、go test --count=1 --shuffle=on -race -cover、release build、機密ファイルのstage防止、gitleaks detect --no-git --source . を順に実行します。RUN_E2E=1では28本のtests/e2e/*.shも実行します。gitleaksはstage状態にかかわらず作業ツリーを検査し、.gitleaks.tomlのallowlistに列挙したpathとマスク済みplaceholderを対象外にします。

変更行のmutation testingは、比較元commitと1つ以上のGo packageを指定して実行します。

bash scripts/quality/mutation-diff.sh <base-ref> <go-package> [<go-package> ...]

各引数はgo listで単一のGo packageへ解決されます。tests/e2e/配下はgremlinsを起動せず、E2Eスクリプトで検証します。.gremlins.yamlは、x/termフォーク基底のterminal.goと、リポジトリルートを対象にしたgremlins実行時のtests/e2e/を変異対象から除外します。

gremlinsは作業ツリー全体をworkerごとの一時ディレクトリへコピーします。GGUF、checkpoint、生成動画などの大容量ファイルはリポジトリ外に保存してください。中断後に$TMPDIR/gremlins-*が残った場合は、gremlinsプロセスが動いていないことを確認してから削除してください。

ライセンス

MIT

バージョン履歴

v0.13.0までは「マージ済みPR 1本=1バージョン」として遡って対応づけたものです。以降はConventional Commitsに基づく自動bumpで付与します。

バージョン 日付 機能の追加・修正
v0.14.0 2026-08-30 メモリ機能(階層AGENTS.md連結と@import、自動メモリmemory_write/memory_read/memory#)、semantic versionの自動付与、fsxパッケージ(O_NOFOLLOW付きの安全なopen)
v0.13.0 2026-08-30 改善1-10と日本語UX(compaction、session resume、AGENTS.md自動読み込み、行エディタ内蔵、/help /model /cost /clear /tools、E2E 27本、変異テストとCRAP計測)
v0.12.0 2026-08-15 対話UX・安全性・品質ゲートの強化
v0.11.0 2026-08-10 Web検索とfollow-upの信頼性向上
v0.10.0 2026-08-09 web_search / web_fetchツール
v0.9.0 2026-08-09 日本語IME復元(cooked入力と生成中のみraw監視のハイブリッドREPL)
v0.8.0 2026-08-09 raw-mode行エディタ(ESCキャンセル、履歴、CJK幅)
v0.7.0 2026-08-08 OpenAI互換ツール定義にparametersとdescriptionを付与
v0.6.0 2026-08-08 llama.cpp(llama-server)プロバイダ
v0.5.0 2026-07-05 reusable-workflows@v1による共通CI
v0.4.0 2026-07-05 依存更新(golang.org/x/net 0.55.0)
v0.3.0 2026-05-23 REPLの進捗スピナーとターン要約
v0.2.0 2026-05-23 本番投入に向けた16領域のハードニング
v0.1.0 2026-05-21 ツール層のdeny-by-defaultとハードニング

About

Go 1.25製のCGOなし単一バイナリAIエージェント

Topics

Resources

Stars

4 stars

Watchers

0 watching

Forks

Releases

Used by

Contributors

Languages