AIエージェントの費用というと、「たくさん書かせると高くなる」——つまり出力の量で決まると考えがちです。
Codex CLI が手元に残している作業記録を全部数えてみると、逆でした。99回分の実行で、トークンの99.2%は入力。しかもその93%はキャッシュからの読み直しで、AIが書いた出力は0.8%しかありませんでした。
この記事は、その数え方です。Codex CLI は、何もしなくても作業の記録を JSONL で残しています。これを読むだけで、どの道具を何回使い、どのコマンドを打ち、どれだけトークンを使ったかが分かります。
記録はどこにあるか
~/.codex/sessions/YYYY/MM/DD/rollout-<日時>-<id>.jsonl
1回の実行(セッション)が1ファイルです。1行に1レコードで、type と payload.type で種類が分かれます。今回使うのは次の4つだけです。
| レコード | 中身 |
|---|---|
session_meta(1行目) |
作業ディレクトリ(cwd)など。どの仕事のセッションかを絞り込める |
response_item / custom_tool_call
|
道具の呼び出し。中身は小さな JavaScript |
response_item / custom_tool_call_output
|
道具の結果。call_id で呼び出しと結び付く |
event_msg / token_count
|
そのセッションのトークン使用量の累計 |
道具の呼び出しは「小さなJSコード」として残る
custom_tool_call の input には、たとえば次のようなコードが入っています。
const r = await tools.exec_command({cmd: "rg -n 'def route' src/app.py"});
text(await r.output());
1回の呼び出しの中で、複数の道具を続けて呼ぶこともあります(今回のデータでは最大5つ)。なので「先頭の単語」を数えると const が最多になってしまいます。tools.<名前>( と cmd: "..." を全部拾うのがコツです。
トークンは累計で入る
token_count の info.total_token_usage には、そのセッションの累計が入ります。
{"input_tokens": 32547, "cached_input_tokens": 0, "output_tokens": 139, "reasoning_output_tokens": 18, "total_tokens": 32686}
途中の値を足すと二重に数えるので、各セッションの最後の値だけを使います。
数えるスクリプト
外部ライブラリは使いません。引数に、作業ディレクトリに含まれる文字列を渡すと、その仕事のセッションだけを数えます。
#!/usr/bin/env python3
"""Codex CLI の作業記録(~/.codex/sessions/**/rollout-*.jsonl)を数える。
python3 codex_usage.py [作業ディレクトリに含まれる文字列]
"""
import glob, json, os, re, statistics, sys
from collections import Counter
keep = sys.argv[1] if len(sys.argv) > 1 else ""
tools, commands, usage, sessions, broken = Counter(), Counter(), [], 0, 0
for path in sorted(glob.glob(os.path.expanduser("~/.codex/sessions/*/*/*/rollout-*.jsonl"))):
with open(path, encoding="utf-8", errors="replace") as f:
lines = f.read().splitlines()
try:
meta = json.loads(lines[0]).get("payload", {}) # 1行目は session_meta
except (ValueError, IndexError):
continue
if keep not in str(meta.get("cwd", "")):
continue
sessions += 1
last = None
for line in lines:
try:
rec = json.loads(line)
except ValueError: # 書き込み途中などで壊れた行は飛ばす
broken += 1
continue
p = rec.get("payload") or {}
if p.get("type") == "custom_tool_call": # 道具の呼び出し(中身は小さなJSコード)
src = str(p.get("input") or "")
tools.update(re.findall(r"tools\.(\w+)\s*\(", src))
for m in re.findall(r'cmd\s*:\s*"((?:[^"\\]|\\.)*)"', src):
words = [w for w in json.loads('"' + m + '"').split() if "=" not in w]
if words:
commands[os.path.basename(words[0])] += 1
if p.get("type") == "token_count" and p.get("info"):
last = p["info"].get("total_token_usage") # セッション内の累計。最後の値を使う
if last:
usage.append(last)
total = lambda k: sum(u.get(k, 0) for u in usage)
inp, cached, out = total("input_tokens"), total("cached_input_tokens"), total("output_tokens")
print(f"sessions: {sessions} broken lines skipped: {broken}")
print(f"tools: {tools.most_common(6)}")
print(f"commands: {commands.most_common(8)}")
print(f"input {inp:,} (cached {cached:,} = {100*cached/max(1,inp):.0f}%) output {out:,} = {100*out/max(1,inp+out):.1f}% of all")
print(f"per session median: input {statistics.median(u.get('input_tokens',0) for u in usage):,.0f}"
f" / output {statistics.median(u.get('output_tokens',0) for u in usage):,.0f}")
結果(自分の環境、2026年8〜10月の99セッション)
sessions: 99 broken lines skipped: 87
tools: [('exec_command', 1065), ('apply_patch', 143), ('web__run', 79), ('write_stdin', 57), ('mcp__codex_apps__google_drive_fetch', 5), ('mcp__codex_apps__remote_desktop_commander_read_file', 1)]
commands: [('python3', 327), ('cat', 166), ('rg', 117), ('sed', 70), ('git', 55), ('python', 32), ('find', 31), ('pytest', 27)]
input 69,760,539 (cached 64,894,720 = 93%) output 586,071 = 0.8% of all
per session median: input 559,265 / output 7,308
トークン:出力は0.8%
| トークン | |
|---|---|
| 入力 | 69,760,539 |
| うちキャッシュ | 64,894,720(入力の93%) |
| 出力 | 586,071(全体の0.8%) |
| 1セッションの中央値 | 入力 559,265 / 出力 7,308 |
Codex は作業のたびに、指示・これまでのやり取り・読んだファイルの中身を読み直しています。だから入力が圧倒的に大きく、その大半がキャッシュに乗っています。費用や使用量の上限を考えるときに見るべきなのは、出力の長さより、1回のセッションで何を読み込ませているかでした。
道具:ほぼ「コマンドを打つ」
| 道具 | 回数 |
|---|---|
| exec_command(シェルのコマンド) | 1,065 |
| apply_patch(ファイルの書き換え) | 143 |
| web__run(Web) | 79 |
| write_stdin(実行中のプロセスへの入力) | 57 |
コマンド:多くは「読む」
| コマンド | 回数 |
|---|---|
| python3 | 327 |
| cat | 166 |
| rg | 117 |
| sed | 70 |
| git | 55 |
| python | 32 |
| find | 31 |
| pytest | 27 |
cat・rg・sed・find を合わせると384回。書き換え(apply_patch 143回)の2倍以上、確認のために読んでいます。
ハマったところ
-
壊れた行がある:書き込みの途中などで JSON として読めない行が、99セッション中で87行ありました。1行ずつ
tryで読み、飛ばして数を出しています。 -
作業ディレクトリで絞る:Codex のデスクトップアプリなど、別の用途のセッションも同じ場所に保存されます。1行目の
cwdで絞らないと、仕事以外の会話まで混ざります。 - 形式は公式の仕様ではない:CLI の内部の記録なので、バージョンで変わる可能性があります。キーが無いときに落ちないように書いておくのが安全です。
まとめ
- Codex CLI の作業記録は
~/.codex/sessions/の JSONL。1行目のcwdで仕事ごとに絞れる - 道具の呼び出しは小さな JS。
tools.<名前>(とcmd: "..."を全部拾う - トークンは
token_countの累計の最後の値を使う - 自分の99セッションでは、トークンの99.2%が入力、その93%がキャッシュ、出力は0.8%
AIの使用量を減らしたいなら、まず「何を読ませているか」を数えるところからだと思います。