요약
- Claude Code 서브에이전트는 파일이 지정하지 않는 한 Haiku 5.5에서 돌지 않습니다. 내장 Explore 에이전트와 모든 커스텀 서브에이전트는 메인 모델을 상속하며, 서브에이전트 모델 변수만으로는 Explore가 바뀌지 않습니다.
- 파일 두 개로 해결됩니다. 탐색기만 옮기는 프로젝트
Explore.md에이전트 파일(Haiku 5.5, effort low)과, 모든 서브에이전트를 Haiku로 밀어 넣는CLAUDE_CODE_SUBAGENT_MODEL_FORCE가 든settings.jsonenv 블록입니다. - 실제 레포 하나에서 182번 돌린 매트릭스: 탐색, 테스트, 린트, 커밋, 리팩터, 리뷰, 요약 모두 Haiku 5.5에서 숨겨진 테스트나 고정 규칙으로 통과했고, low와 medium effort에서 각각 70번 중 70번입니다.
- 일곱 개 모두 effort low의 Haiku 5.5로 보내십시오. 복잡한 에이전트형 코딩은 Sonnet 5.5나 Opus 5.5에 남기십시오. Anthropic 자신의 조언입니다.
- 실제 플랜 미터는 거의 움직이지 않았습니다. Haiku low 70번 동안 5시간 창은 21%에서 21%, 주간 창은 182번 내내 67%였습니다.
/usage분석은 청구가 아니라 로컬 가중치로 모델의 순위를 매깁니다. - 전체 모델 id를 고정하십시오.
haiku별칭은 Bedrock, Vertex, Foundry에서 Haiku 4.5입니다.
출처가 말하는 것
Haiku 5.5의 가격은 100k 토큰 이하 프롬프트에서 입력 100만 토큰당 $0.10, 그 이상에서 $0.50이며, 출력은 $0.50과 $2.50입니다. Haiku 4.5는 $1.00과 $5.00, Sonnet 5.5는 $2.00과 $10.00입니다 s1. 가격 페이지는 Opus 5.5를 100만 토큰당 $4와 $20으로 싣고, 다른 1M 컨텍스트 모델과 달리 Haiku 5.5는 프롬프트 길이로 가격이 매겨진다고 적습니다 s20. 헤드라인의 90%는 이전 Haiku 요청 중 프롬프트가 100k 토큰 미만인 비율이며, 가격 인하가 아닙니다 s1.
Anthropic은 Haiku 5.5를 Opus 5.5, Sonnet 5.5 옆의 서브에이전트로 자리매김하고, 복잡한 에이전트형 코딩에는 Sonnet 5.5와 Opus 5.5가 여전히 더 좋은 선택이며 Haiku 5.5는 범위가 좁은 작업용이라고 말합니다 s1. Terminal-Bench 4.0에서 출시 글은 Haiku 5.5에 39.2%, Sonnet 5.5에 70.6%를 제시합니다 s1.
기본값으로 Haiku에 가는 것은 없습니다. v2.1.198부터 내장 Explore 에이전트는 Haiku를 기본으로 쓰지 않고 메인 모델을 상속합니다 s24. 문서화된 서브에이전트 모델 해석 순서는 호출별 매개변수, 정의의 frontmatter, CLAUDE_CODE_SUBAGENT_MODEL, 메인 대화의 모델입니다 s2. CLAUDE_CODE_SUBAGENT_MODEL만으로는 내장 Explore와 Plan 서브에이전트가 바뀌지 않습니다 s2. Explore라는 이름의 사용자 또는 프로젝트 서브에이전트가 내장을 덮어쓰며, 문서는 Haiku 모델로 하나 정의하라고 제안합니다 s2. CLAUDE_CODE_SUBAGENT_MODEL_FORCE=1은 v2.1.257부터 모든 서브에이전트에 모델 하나를 적용합니다 s2 s24.
비용 문서는 단순한 서브에이전트 작업에 model: haiku를 권하고, 서브에이전트 요청도 같은 사용량을 소모한다고 명시합니다 s3. 주간 미터는 모든 모델용 하나와, 포함된 경우 Fable용 하나입니다 s22. 다른 모델도 같은 한도에서 차감됩니다 s23.
Claude Code에서 Haiku 5.5, Opus 5.5, Sonnet 5.5, Fable의 thinking은 끌 수 없습니다. alwaysThinkingEnabled를 false로 두거나 MAX_THINKING_TOKENS=0으로 해도 효과가 없습니다. effort는 low에서 max까지이고 Haiku 5.5의 기본값은 medium입니다 s21. haiku 별칭은 Anthropic API에서 Haiku 5.5로, AWS 플랫폼, Bedrock, Vertex, Foundry에서는 Haiku 4.5로 해석됩니다 s21.
한 실무자의 Pro 플랜 구성(medium의 Opus 5.5 메인, Sonnet 5.5 구현자, 읽기 전용 Haiku 5.5 스코퍼, low의 Haiku 5.5 shipper)은 10개 세션에 걸친 PR 13개를 보고하며, PR 하나당 5시간 창의 약 6%, PR 하나짜리 세션의 주간 카운터 약 1%라고 합니다 s4. 본인의 단서는 이렇습니다. 절감은 더 싼 모델이 아니라 메인 컨텍스트가 작게 유지된 데서 나오며, 서브에이전트 토큰을 Opus 요율로 계산해도 5%만 늘어납니다 s4.
파일 두 개
파일 두 개이며, 하나만 써도 둘 다 써도 됩니다. 프로젝트 에이전트 파일 .claude/agents/Explore.md는 이름이 Explore이므로 내장 탐색기를 대체합니다. 전체 모델 id, effort low, 읽기와 검색 도구만 둡니다.
Explore.md
---
name: Explore
description: Read-only codebase explorer. Use it to find where something is defined or called and to summarize what it found, with file paths and function names.
model: claude-haiku-5-5
effort: low
tools: Read, Glob, Grep, Bash
---
You explore a codebase and report back. Search with Glob and Grep, read only the files you need, and never edit anything. Answer with file paths and function names, then stop.
settings 파일 .claude/settings.json에는 env 블록이 들어 있습니다. 서브에이전트 모델과, 그것을 모든 서브에이전트에 적용하게 하는 force 플래그입니다.
settings.json
{
"env": {
"CLAUDE_CODE_SUBAGENT_MODEL": "claude-haiku-5-5",
"CLAUDE_CODE_SUBAGENT_MODEL_FORCE": "1"
}
}
누구에게 어느 쪽인가:
- 모든 서브에이전트를 저렴하게 하려면 env 블록을 쓰십시오. FORCE는 일부러 Opus로 지정한 에이전트까지 Haiku로 끌어내립니다.
- 탐색기만 옮기려면 Explore 파일을 쓰십시오. 다른 서브에이전트는 각자의 모델을 유지합니다.
- FORCE 없이
CLAUDE_CODE_SUBAGENT_MODEL만 설정하면 Explore에는 아무 효과가 없습니다. 우리가 한 헤드리스 실행 두 번, 메인 세션은 Fable이고 "Explore 에이전트로 쿼리 캐시가 비워지는 위치를 찾아 줘"라는 요청: 설정이 없으면 툴 결과는 Opus 5.5였고, 변수만 Haiku 5.5로 설정해도 다시 Opus 5.5였습니다. Explore 파일을 쓰거나 env 변수 두 개를 쓰면 Haiku 5.5로 읽혔습니다.
확인 방법: /tasks는 각 서브에이전트가 돈 모델과 effort를 보여 주고(v2.1.243 이상 s24), 헤드리스 실행의 Agent 툴 결과에는 resolvedModel이 담깁니다. 그 줄을 믿고, 파일은 믿지 마십시오.
별칭의 함정: haiku가 아니라 claude-haiku-5-5를 쓰십시오. 별칭은 Anthropic API에서만 Haiku 5.5이고, Bedrock, Vertex, Foundry에서는 여전히 Haiku 4.5를 뜻합니다 s21.
더 오래된 열린 보고는 서브에이전트 frontmatter의 모델이 조용히 무시되었다고 하고 s25, 더 나중의 보고는 2.1.277부터 2.1.290에서 서브에이전트 1,371개 중 1,149개가 부모의 Opus 모델로 돌았다고 집계합니다 s10. Claude Code 2.1.293에서는 우리 182번 실행 모두 설정한 모델을 달고 있었습니다(결과 표의 model_ok가 True). 그래도 확인하십시오.
라우팅 표
tinydb 18d73a1에서 일상 작업 일곱 개, 테스트 226개, 헤드리스 claude -p, 작업마다 결정적인 통과 또는 거부 검사 하나. 각 셀은 통과한 실행 수, 실행당 평균 정가, 평균 소요 초입니다. Haiku 셀은 작업당 10번, Sonnet과 Opus 셀은 작업당 3번입니다.
| 작업 | Haiku 5.5 medium | Haiku 5.5 low | Sonnet 5.5 | Opus 5.5 | 경로 |
|---|---|---|---|---|---|
| explore | 10/10, $0.0047, 13.4초 | 10/10, $0.0035, 10.7초 | 3/3, $0.0679, 30.0초 | 3/3, $0.1454, 18.0초 | Haiku 5.5, effort low |
| tests | 10/10, $0.0033, 9.8초 | 10/10, $0.0031, 9.0초 | 3/3, $0.0531, 20.0초 | 3/3, $0.0892, 14.3초 | Haiku 5.5, effort low |
| lint | 10/10, $0.0048, 16.7초 | 10/10, $0.0041, 14.0초 | 3/3, $0.0693, 23.3초 | 3/3, $0.1459, 27.7초 | Haiku 5.5, effort low |
| commit | 10/10, $0.0017, 5.1초 | 10/10, $0.0018, 4.7초 | 3/3, $0.0313, 7.0초 | 3/3, $0.0593, 5.7초 | Haiku 5.5, effort low |
| refactor | 10/10, $0.0078, 26.3초 | 10/10, $0.0052, 17.1초 | 3/3, $0.1050, 30.3초 | 3/3, $0.1675, 30.3초 | Haiku 5.5, effort low |
| review | 10/10, $0.0040, 16.0초 | 10/10, $0.0030, 10.6초 | 3/3, $0.0500, 20.3초 | 3/3, $0.0951, 17.3초 | Haiku 5.5, effort low |
| summary | 10/10, $0.0037, 12.4초 | 10/10, $0.0032, 8.4초 | 3/3, $0.0347, 8.7초 | 3/3, $0.0701, 7.7초 | Haiku 5.5, effort low |
| 일곱 개 전체 | 70/70, 합계 $0.3006 | 70/70, 합계 $0.2382 | 21/21, 합계 $1.2339 | 21/21, 합계 $2.3175 |
"refactor" 작업은 작은 기능입니다. tinydb.operations에 push와 pull 업데이트 연산을 추가하는 것이며, 숨겨진 테스트 파일로 채점합니다. Haiku low에서 $0.0052, Opus에서 $0.1675가 들었고, 소요 시간은 17.1초 대 30.3초였습니다.
Sonnet이나 Opus에 남는 것: Anthropic 자신의 표현으로 복잡한 에이전트형 코딩 s1. 이 일곱 개는 작은 레포 하나에서 각각 1분 미만인 범위가 정해진 작업입니다.
하네스
표를 만든 벤치마크입니다. 플랜 미터가 공유되므로 계정을 다른 것이 쓰지 않는 일반 터미널에서 실행하십시오. tinydb 체크아웃, 숨겨진 테스트, 참조 패치는 포함되어 있지 않습니다. 자기 레포에 맞추고 자기 검사를 작성하십시오.
러너와 미터:
README.md: 하네스가 무엇이고 무엇이 빠졌는지.README.md
# routing bench
The benchmark harness behind the routing table: run.sh runs one cell headless and reads the real meters from the rate_limit_event, matrix.sh runs cells sequentially, check.py decides accept or reject per task, analyze.py writes the results table. tasks/ holds the seven prompts, inject/ the planted bugs. The tinydb checkout (commit 18d73a1), the hidden tests and the reference patches are not included: bring your own repo and checks.
- `run.sh`: 셀 하나, 고정된 모델의 헤드리스 `claude -p`, 새 작업 복사본, 실행 후 미터 프로브, 그리고 검사. run.sh
#!/usr/bin/env bash
# One run: fresh tinydb work copy + task injection, headless claude -p, extract tokens/meter, check.
# Usage: run.sh <config> <task> <rep>
# config: a key of configs.json (haiku-med | haiku-low | sonnet | opus)
# task: a key of tasks.json (explore | tests | lint | commit | refactor | review | summary)
set -uo pipefail
CFG="$1"; TASK="$2"; REP="${3:-1}"
HERE="$(cd "$(dirname "$0")" && pwd)"
. "$HERE/env.sh"
PY="$HERE/.venv/bin/python"
MAX_TURNS="${BENCH_MAX_TURNS:-40}"
NAME="${TASK}-${CFG}-r${REP}"
OUT="$HERE/runs/$NAME"; W="$HERE/work/$NAME"
rm -rf "$OUT"; mkdir -p "$OUT"
"$HERE/prep.sh" "$TASK" "$W" > "$OUT/prep.txt" 2>&1 || { echo "$NAME: prep failed"; cat "$OUT/prep.txt"; exit 2; }
( cd "$W" && git diff HEAD > "$OUT/pre.diff" )
# Config args (model pinned by full id, optional --effort)
CARGS=()
while IFS= read -r a; do CARGS+=("$a"); done < <("$PY" -c "
import json; [print(a) for a in json.load(open('$HERE/configs.json'))['$CFG']['args']]")
[ ${#CARGS[@]} -gt 0 ] || { echo "bad config $CFG" >&2; exit 2; }
# Fixed allow-list for every cell, including Bash(ruff:*) for the lint task.
TOOLS="Read,Edit,Write,MultiEdit,Glob,Grep,Skill,TodoWrite,Bash(uv:*),Bash(python:*),Bash(python3:*),Bash(pytest:*),Bash(ruff:*),Bash(git status:*),Bash(git diff:*),Bash(git log:*),Bash(ls:*),Bash(cat:*),Bash(mkdir:*),Bash(mktemp:*),Bash(head:*),Bash(tail:*),Bash(grep:*),Bash(wc:*)"
cd "$W"
START=$(date +%s)
claude -p "$(cat "$HERE/tasks/$TASK.md")" "${CARGS[@]}" \
--output-format stream-json --verbose --max-turns "$MAX_TURNS" \
--permission-mode acceptEdits --allowedTools "$TOOLS" \
--setting-sources project --strict-mcp-config \
> "$OUT/stream.jsonl" 2> "$OUT/stderr.txt"
echo "$?" > "$OUT/exit_code.txt"
END=$(date +%s)
echo "$((END-START))" > "$OUT/wall_s.txt"
# After-meter: one cheap Haiku probe (its own stream is kept for audit).
"$HERE/probe.sh" "$OUT/probe.jsonl" > "$OUT/after.json" 2>/dev/null
# Diff of the agent's work (tracked + untracked) vs the post-injection state
# (index left untouched: the commit task's staged changes stay staged for the check)
git diff HEAD -- . ':(exclude).coverage' > "$OUT/post.diff"
git diff HEAD --numstat -- . ':(exclude).coverage' > "$OUT/numstat.txt"
git ls-files --others --exclude-standard | while IFS= read -r f; do
git diff --no-index -- /dev/null "$f" >> "$OUT/post.diff"
echo "$(wc -l < "$f" | tr -d ' ') 0 $f" >> "$OUT/numstat.txt"
done
"$PY" "$HERE/extract.py" "$OUT" "$CFG" "$TASK" "$REP" > "$OUT/result.json"
"$PY" "$HERE/check.py" "$TASK" "$W" "$OUT/result.json" > "$OUT/check.txt"; echo "$?" >> "$OUT/check.txt"
"$PY" "$HERE/extract.py" "$OUT" "$CFG" "$TASK" "$REP" --score
- `matrix.sh`: 정렬된 셀을 차례로 실행하고 로그에 덧붙입니다. matrix.sh
#!/usr/bin/env bash
# Run an ordered list of "<config> <task> <rep>" lines SEQUENTIALLY (the plan meter is shared:
# never run two cells at once). Blank lines and #comments are skipped. Appends to matrix.log.
# Usage: matrix.sh [matrix-file] (default: matrix.txt)
# SKIP_DONE=1 matrix.sh ... skip cells whose runs/<task>-<config>-r<rep>/score.json exists (resume)
set -uo pipefail
HERE="$(cd "$(dirname "$0")" && pwd)"; cd "$HERE"
LIST="${1:-matrix.txt}"
# one matrix at a time: the meter is shared
mkdir "$HERE/.matrix.lock" 2>/dev/null || { echo "another matrix.sh is running (remove .matrix.lock if stale)" >&2; exit 1; }
trap 'rmdir "$HERE/.matrix.lock"' EXIT
echo "=== $(date '+%F %T') start $LIST" >> matrix.log
grep -vE '^\s*(#|$)' "$LIST" | while read -r CFG TASK REP; do
if [ "${SKIP_DONE:-0}" = 1 ] && [ -f "runs/${TASK}-${CFG}-r${REP}/score.json" ]; then
echo "$(date '+%F %T') skip ${TASK}-${CFG}-r${REP}" >> matrix.log; continue
fi
line=$(./run.sh "$CFG" "$TASK" "$REP" < /dev/null 2>&1 | tail -1)
echo "$(date '+%F %T') $line" | tee -a matrix.log
done
echo "=== $(date '+%F %T') done $LIST" >> matrix.log
- `gen_matrix.py`: 정렬된 매트릭스를 만듭니다. 각 구성은 연속된 블록이고, 저렴한 구성이 먼저이며, 두 번째 패스는 역순입니다. gen_matrix.py
#!/usr/bin/env python3
"""Write the ordered matrix file.
gen_matrix.py [--reps 3] [--passes 2] [--configs haiku-low,haiku-med,sonnet,opus] [--out matrix.txt]
Order: configs run as contiguous BLOCKS (all tasks of one rep set), cheap configs first; tasks are
rotated inside each block so no task always runs first. With --passes 2 the second pass runs the
configs in reverse order (ABBA), which interleaves configs over time without breaking the
contiguous segments that analyze.py needs to beat the 1% meter resolution.
"""
import argparse, json, os
HERE = os.path.dirname(os.path.abspath(__file__))
ap = argparse.ArgumentParser()
ap.add_argument("--reps", type=int, default=3, help="reps per config x task per pass")
ap.add_argument("--passes", type=int, default=2)
ap.add_argument("--configs", default="haiku-low,haiku-med,sonnet,opus")
ap.add_argument("--start-rep", type=int, default=1, help="first rep number (use 2 to keep the r1 smoke runs)")
ap.add_argument("--out", default=os.path.join(HERE, "matrix.txt"))
a = ap.parse_args()
tasks = list(json.load(open(os.path.join(HERE, "tasks.json"))))
cfgs = a.configs.split(",")
lines, rep = [], a.start_rep
for p in range(a.passes):
order = cfgs if p % 2 == 0 else list(reversed(cfgs))
lines.append(f"# pass {p + 1}: {' -> '.join(order)}")
for c in order:
lines.append(f"# block {c}")
for r in range(a.reps):
k = rep + r
rot = (k - 1) % len(tasks)
for t in tasks[rot:] + tasks[:rot]:
lines.append(f"{c} {t} {k}")
rep += a.reps
open(a.out, "w").write("\n".join(lines) + "\n")
n = sum(1 for l in lines if not l.startswith("#"))
print(f"{a.out}: {n} runs ({len(cfgs)} configs x {len(tasks)} tasks x {a.reps} reps x {a.passes} passes)")
- `prep.sh`: 새 작업 복사본을 만들고 작업에 심어 둔 변경을 적용합니다. prep.sh
#!/usr/bin/env bash
# Build a fresh work copy for one task: upstream tinydb, baseline commit, task injection.
# Usage: prep.sh <task> <workdir>
set -euo pipefail
HERE="$(cd "$(dirname "$0")" && pwd)"
TASK="$1"; W="$2"
read -r INJ MODE < <("$HERE/.venv/bin/python" -c "
import json,sys; t=json.load(open('$HERE/tasks.json'))['$TASK']; print(t['inject'] or '-', t['mode'] or '-')")
rm -rf "$W"; mkdir -p "$(dirname "$W")"
cp -R "$HERE/upstream" "$W"
cd "$W"
/usr/bin/find . -name __pycache__ -type d -prune -exec rm -rf {} +
rm -f .coverage
git update-index -q --refresh || true
G() { git -c user.email=dev@example.com -c user.name=dev "$@"; }
# Injections that belong to the baseline are folded into the top upstream commit
# (amend, same message/author/date), so `git log` shows no extra "bug" commit.
if [ "$MODE" = "commit" ]; then
git apply "$HERE/$INJ"
git add -A
G commit -q --amend --no-edit --no-verify
fi
case "$MODE" in
stage) git apply --index "$HERE/$INJ" ;;
worktree) git apply "$HERE/$INJ" ;;
esac
- `env.sh`: 모든 호출이 공유하는 인증과 환경. env.sh
# Sourced by run.sh and probe.sh.
# Auth: an isolated CLAUDE_CONFIG_DIR is not logged in
# (`claude auth status` -> loggedIn false), so runs use the user's default config dir:
# CLAUDE_CONFIG_DIR is deliberately left unset.
unset CLAUDE_CONFIG_DIR
# Fresh work copies at the same path must not inherit memory written by an earlier rep.
export CLAUDE_CODE_DISABLE_AUTO_MEMORY=1
# The bench venv (pytest, ruff, mypy) comes first, so `python -m pytest` and `ruff` just work.
export PATH="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/.venv/bin:$PATH"
- `probe.sh`: 현재 플랜 미터를 JSON으로 출력하는 Haiku 턴 하나. probe.sh
#!/usr/bin/env bash
# Cheapest possible call: one Haiku turn answering "ok". Prints the current plan
# meter read from its rate_limit_event as one JSON line. The probe itself costs a
# few hundred tokens of Haiku (well under the 1% meter resolution).
# Usage: probe.sh [raw-stream-out-file]
set -uo pipefail
HERE="$(cd "$(dirname "$0")" && pwd)"
[ -f "$HERE/env.sh" ] && . "$HERE/env.sh"
RAW="${1:-$(mktemp -t hrb-probe)}"; case "$RAW" in /*) ;; *) RAW="$PWD/$RAW";; esac
cd "$(mktemp -d -t hrb-probe-cwd)"
claude --model claude-haiku-5-5 -p "ok" --max-turns 1 --output-format stream-json --verbose \
--setting-sources project --strict-mcp-config > "$RAW" 2>/dev/null
"$HERE/.venv/bin/python" "$HERE/meter.py" "$RAW"
- `meter.py`: 스트림의 `rate_limit_event` 줄에서 5시간 창과 7일 창을 읽습니다. meter.py
#!/usr/bin/env python3
"""Read a stream-json file and print the plan meter carried by its rate_limit_event lines.
Output (one JSON line): {"ts", "five_hour", "five_hour_resets_at", "seven_day",
"seven_day_resets_at", "n_events", "raw_first", "raw_last"}: utilizations are the
values of the FIRST event (the meter as the run began). `--last` uses the last event.
"""
import json, sys, time
def events(path):
out = []
for line in open(path, errors="replace"):
line = line.strip()
if not line.startswith("{"):
continue
try:
o = json.loads(line)
except Exception:
continue
if o.get("type") == "rate_limit_event":
out.append(o)
return out
def windows(ev):
info = ev.get("rate_limit_info") or {}
uw = info.get("unifiedWindows") or {}
def w(name):
x = uw.get(name) or {}
return x.get("utilization"), x.get("resetsAt")
fh, fhr = w("five_hour")
sd, sdr = w("seven_day")
return {"five_hour": fh, "five_hour_resets_at": fhr, "seven_day": sd, "seven_day_resets_at": sdr}
def meter(path, which="first"):
evs = events(path)
if not evs:
return {"ts": int(time.time()), "n_events": 0, "five_hour": None, "seven_day": None}
ev = evs[0] if which == "first" else evs[-1]
d = {"ts": int(time.time()), "n_events": len(evs)}
d.update(windows(ev))
d["raw_first"] = evs[0].get("rate_limit_info")
d["raw_last"] = evs[-1].get("rate_limit_info")
return d
if __name__ == "__main__":
which = "last" if "--last" in sys.argv else "first"
path = [a for a in sys.argv[1:] if not a.startswith("--")][0]
print(json.dumps(meter(path, which)))
- `extract.py`: 실행 스트림에서 토큰, 비용, 턴, 미터를 뽑아 점수 레코드로 만듭니다. extract.py
#!/usr/bin/env python3
"""Extract one run's numbers from runs/<name>/stream.jsonl.
extract.py <outdir> <config> <task> <rep> -> prints the stream's `result` line (result.json)
extract.py <outdir> <config> <task> <rep> --score -> writes score.json, prints a one-line summary
"""
import json, os, sys
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
from meter import meter # noqa: E402
out, cfg, task, rep = sys.argv[1:5]
score_mode = "--score" in sys.argv
stream = os.path.join(out, "stream.jsonl")
lines = []
for raw in open(stream, errors="replace") if os.path.exists(stream) else []:
raw = raw.strip()
if raw.startswith("{"):
try:
lines.append(json.loads(raw))
except Exception:
pass
result = next((o for o in reversed(lines) if o.get("type") == "result"), None)
init = next((o for o in lines if o.get("type") == "system" and o.get("subtype") == "init"), {})
if not score_mode:
print(json.dumps(result or {"type": "result", "result": "", "missing": True}))
sys.exit(0)
def rd(name, default=None):
p = os.path.join(out, name)
try:
return open(p).read().strip()
except Exception:
return default
def jl(name):
try:
return json.loads(rd(name) or "null")
except Exception:
return None
cfgs = json.load(open(os.path.join(HERE, "configs.json")))
before = meter(stream, "first") if os.path.exists(stream) else None
after = jl("after.json")
r = result or {}
usage = r.get("usage") or {}
tok = {k: usage.get(k) or 0 for k in ("input_tokens", "cache_read_input_tokens", "cache_creation_input_tokens", "output_tokens")}
mu = r.get("modelUsage") or {}
assistant_models = sorted({(o.get("message") or {}).get("model") for o in lines
if o.get("type") == "assistant" and (o.get("message") or {}).get("model") not in (None, "<synthetic>")})
check_txt = (rd("check.txt", "") or "").split("\n")
verdict_line = check_txt[0] if check_txt else ""
numstat = [l.split("\t") for l in (rd("numstat.txt", "") or "").split("\n") if l.strip()]
added = sum(int(a) for a, d, f in numstat if a.isdigit())
deleted = sum(int(d) for a, d, f in numstat if d.isdigit())
changed_by_agent = (rd("pre.diff", "") or "") != (rd("post.diff", "") or "")
def strip_raw(m):
if not m:
return m
return {k: v for k, v in m.items() if not k.startswith("raw_")}
s = {
"run": os.path.basename(out), "config": cfg, "task": task, "rep": int(rep),
"expected_model": cfgs[cfg]["model"], "config_args": cfgs[cfg]["args"],
"init_model": init.get("model"), "assistant_models": assistant_models,
"model_usage_keys": sorted(mu.keys()),
"model_ok": init.get("model") == cfgs[cfg]["model"] and set(mu.keys()) <= {cfgs[cfg]["model"]} and bool(mu),
"claude_code_version": init.get("claude_code_version"),
"meter_before": strip_raw(before), "meter_after": strip_raw(after),
"n_rate_limit_events": (before or {}).get("n_events", 0),
"has_result_line": result is not None,
"subtype": r.get("subtype"), "is_error": r.get("is_error"), "stop_reason": r.get("stop_reason"),
"num_turns": r.get("num_turns"), "duration_ms": r.get("duration_ms"), "duration_api_ms": r.get("duration_api_ms"),
"total_cost_usd": r.get("total_cost_usd"), "usage": usage, "tokens": tok, "modelUsage": mu,
"wall_s": int(rd("wall_s.txt", "0") or 0), "exit_code": rd("exit_code.txt"),
"final_text": (r.get("result") or "")[-2000:],
"diff": {"files": len(numstat), "added": added, "deleted": deleted, "changed_by_agent": changed_by_agent},
"check": {"verdict": verdict_line.split(" ")[0] if verdict_line else None, "reason": verdict_line[5:] if verdict_line else None},
}
s["accepted"] = s["check"]["verdict"] == "PASS"
json.dump(s, open(os.path.join(out, "score.json"), "w"), indent=2)
mb, ma = s["meter_before"] or {}, s["meter_after"] or {}
print(f"{s['run']}: {s['check']['verdict']} turns={s['num_turns']} cost=${(s['total_cost_usd'] or 0):.3f} "
f"in={tok['input_tokens']} cr={tok['cache_read_input_tokens']} cc={tok['cache_creation_input_tokens']} out={tok['output_tokens']} "
f"wall={s['wall_s']}s 5h {mb.get('five_hour')}->{ma.get('five_hour')} 7d {mb.get('seven_day')}->{ma.get('seven_day')} "
f"model_ok={s['model_ok']} | {s['check']['reason']}")
- `check.py`: 작업별 통과 또는 거부 판정. check.py
#!/usr/bin/env python3
"""Deterministic accept/reject check for one run.
Usage: check.py <task> <workdir> <result.json>
result.json = the `result` line of the run's stream (only its "result" text is used).
Prints `PASS <reason>` or `FAIL <reason>`; exit 0 on PASS, 1 on FAIL.
"""
import json, os, re, subprocess, sys
HERE = os.path.dirname(os.path.abspath(__file__))
PY = os.path.join(HERE, ".venv", "bin", "python")
RUFF = os.path.join(HERE, ".venv", "bin", "ruff")
def final_text(path):
try:
r = json.load(open(path))
except Exception:
return ""
return r.get("result") or "" if isinstance(r, dict) else ""
def sh(args, cwd, env=None):
p = subprocess.run(args, cwd=cwd, capture_output=True, text=True, env=env)
return p.returncode, p.stdout + p.stderr
def suite(wd):
rc, out = sh([PY, "-m", "pytest", "-q", "-p", "no:cacheprovider", "-o", "addopts=", "tests"], wd)
m = re.search(r"(\d+) passed", out)
f = re.search(r"(\d+) failed", out)
e = re.search(r"(\d+) error", out)
return rc == 0, f"suite {m.group(1) if m else 0} passed, {f.group(1) if f else 0} failed, {e.group(1) if e else 0} errors"
def git_diff(wd, *paths):
# tracked changes vs the baseline commit + untracked files under paths
_, d = sh(["git", "diff", "HEAD", "--", *paths], wd)
_, u = sh(["git", "ls-files", "--others", "--exclude-standard", "--", *paths], wd)
return d.strip(), u.strip()
def words(s):
return re.findall(r"[A-Za-z0-9_']+", s)
def strip_fences(s):
lines = s.strip().split("\n")
if lines and lines[0].strip().startswith("```"):
lines = lines[1:]
if lines and lines[-1].strip().startswith("```"):
lines = lines[:-1]
return "\n".join(lines).strip()
def c_explore(wd, txt):
t = txt.lower()
miss = []
if "table.py" not in t:
miss.append("table.py")
if "clear_cache" not in t and "_query_cache" not in t:
miss.append("clear_cache/_query_cache")
names = ["insert_multiple", "update_multiple", "insert", "update", "upsert", "remove", "truncate"]
found = {n for n in names if re.search(r"(?<![a-z_])" + n + r"(?![a-z_])", t)}
if len(found) < 3:
miss.append(f"only {len(found)} mutating methods {sorted(found)}")
return (not miss), ("missing " + ", ".join(miss)) if miss else f"table.py + cache method + {len(found)} methods {sorted(found)}"
def c_tests(wd, txt):
ok, s = suite(wd)
d, u = git_diff(wd, "tests")
if d or u:
return False, f"tests/ modified; {s}"
return ok, s + "; tests/ untouched"
def c_lint(wd, txt):
rc, out = sh([RUFF, "check", "tinydb", "--output-format", "concise", "--no-cache"], wd)
cfg_d, _ = git_diff(wd, "ruff.toml", "pyproject.toml")
d, _ = git_diff(wd, "tinydb")
noqa = [l for l in d.split("\n") if l.startswith("+") and "noqa" in l.lower()]
ok, s = suite(wd)
why = []
if rc != 0:
found = [l for l in out.split("\n") if l.startswith("Found ")]
why.append("ruff: " + (found[0] if found else out.strip().split("\n")[-1]))
if cfg_d:
why.append("lint config changed")
if noqa:
why.append(f"{len(noqa)} noqa added")
if not ok:
why.append(s)
return (not why), ("; ".join(why) if why else f"ruff clean; {s}")
DIFF_LINE = re.compile(r"^(diff --git |index [0-9a-f]{6,}\.\.|@@ |\+\+\+ |--- a/|--- /dev/null|[+-] {3,}\S|[+-](def|class|import|from|return) )")
def c_commit(wd, txt):
msg = strip_fences(txt)
if not msg:
return False, "empty answer"
first = msg.split("\n")[0].strip()
why = []
if len(first) > 72:
why.append(f"first line {len(first)} chars")
if "push" not in first.lower():
why.append("no 'push' in first line")
if "pull" not in first.lower():
why.append("no 'pull' in first line")
dl = [l for l in msg.split("\n") if DIFF_LINE.match(l)]
if dl:
why.append(f"{len(dl)} diff lines")
return (not why), ("; ".join(why) if why else f"first line {len(first)} chars: {first!r}")
def c_refactor(wd, txt):
env = dict(os.environ, TINYDB_TREE=wd)
rc, out = sh([PY, "-m", "pytest", "-q", "-c", os.path.join(HERE, "hidden", "pytest.ini"),
"--rootdir", os.path.join(HERE, "hidden"), os.path.join(HERE, "hidden", "test_T1.py")], wd, env)
m = re.search(r"(\d+) passed", out)
hp = int(m.group(1)) if m else 0
ok, s = suite(wd)
return (hp == 5 and ok), f"hidden {hp}/5; {s}"
REVIEW_PATTERNS = [
r"limit\s*=?=\s*0\b",
r"limit\s+(of\s+)?(0|zero)\b",
r"\b(0|zero)\s+limit",
r"zero[- ]limit",
r"limit.{0,150}\b(falsy|falsey|truthy|truthiness)",
r"\b(falsy|falsey|truthy|truthiness)\b.{0,150}limit",
r"if limit:.{0,150}\b(0|zero)\b",
r"\b(0|zero)\b.{0,150}if limit:",
]
def c_review(wd, txt):
t = txt.lower().replace("`", "")
for p in REVIEW_PATTERNS:
m = re.search(p, t, re.S)
if m:
return True, f"matched {p!r}: {m.group(0)[:80]!r}"
return False, "planted limit=0 defect not mentioned"
def c_summary(wd, txt):
n = len(words(txt))
t = txt.lower()
mods = [m for m in ["database", "table", "queries", "storages", "middlewares", "operations"] if m in t]
why = []
if n > 170:
why.append(f"{n} words > 170")
if len(mods) < 4:
why.append(f"only {len(mods)} modules {mods}")
return (not why), ("; ".join(why) if why else f"{n} words, modules {mods}")
CHECKS = {"explore": c_explore, "tests": c_tests, "lint": c_lint, "commit": c_commit,
"refactor": c_refactor, "review": c_review, "summary": c_summary}
if __name__ == "__main__":
task, wd, res = sys.argv[1], os.path.abspath(sys.argv[2]), sys.argv[3]
ok, why = CHECKS[task](wd, final_text(res))
print(("PASS " if ok else "FAIL ") + why)
sys.exit(0 if ok else 1)
- `analyze.py`: 실행을 작업별 및 구성별 표로 집계합니다. analyze.py
#!/usr/bin/env python3
"""Aggregate runs/*/score.json into results.json and results.md.
Per config x task: accept rate, mean tokens, mean cost, mean turns, mean wall.
Per config: measured plan-meter deltas (five_hour, seven_day, in percentage points), computed two ways:
- per_run_sum: sum over the config's runs of (after - before). Quantized at 1% per run, so
runs far below 1% mostly read 0: a lower-bound-ish, very noisy number.
- segments: runs sorted by start time are cut into maximal contiguous segments of the same
config; each segment contributes (after of its last run - before of its first run). This
telescopes the quantization: one +-1 pt error per segment instead of per run. PREFERRED.
Segments/runs whose window resetsAt changed between before and after are excluded for that window.
Plus the /usage-style local heuristic: weighted = cached*1 + uncached*10 + cache_create*12.5 + output*50,
times the model tier (haiku 1 / sonnet 3 / opus 5). The probe's own Haiku tokens after each run are
counted as overhead in the heuristic (they also move the meter).
Usage: analyze.py [--runs-dir runs] [--out-prefix results]
"""
import glob, json, os, statistics, sys
HERE = os.path.dirname(os.path.abspath(__file__))
RUNS = os.path.join(HERE, sys.argv[sys.argv.index("--runs-dir") + 1]) if "--runs-dir" in sys.argv else os.path.join(HERE, "runs")
OUTP = sys.argv[sys.argv.index("--out-prefix") + 1] if "--out-prefix" in sys.argv else "results"
CFGS = json.load(open(os.path.join(HERE, "configs.json")))
TASKS = list(json.load(open(os.path.join(HERE, "tasks.json"))).keys())
W = {"cache_read_input_tokens": 1, "input_tokens": 10, "cache_creation_input_tokens": 12.5, "output_tokens": 50}
TIER = {c: v["tier"] for c, v in CFGS.items()}
def weighted(tok, tier):
raw = sum((tok.get(k) or 0) * w for k, w in W.items())
return raw, raw * tier
MU = {"cacheReadInputTokens": "cache_read_input_tokens", "inputTokens": "input_tokens",
"cacheCreationInputTokens": "cache_creation_input_tokens", "outputTokens": "output_tokens"}
def model_tier(name, fallback):
n = (name or "").lower()
return 5 if "opus" in n else 3 if "sonnet" in n else 1 if "haiku" in n else fallback
def heuristic_run(s):
"""Tiered heuristic per model actually used (modelUsage), so helper calls on another
model are weighted with their own tier; falls back to `usage` x config tier."""
mu = s.get("modelUsage") or {}
if not mu:
return weighted(s["tokens"], TIER[s["config"]])[1]
return sum(weighted({MU[k]: v.get(k) or 0 for k in MU}, model_tier(m, TIER[s["config"]]))[1] for m, v in mu.items())
def probe_tokens(run_dir):
p = os.path.join(run_dir, "probe.jsonl")
if not os.path.exists(p):
return {}
for line in reversed(open(p, errors="replace").read().strip().split("\n")):
try:
o = json.loads(line)
except Exception:
continue
if o.get("type") == "result":
u = o.get("usage") or {}
return {k: u.get(k) or 0 for k in W}
return {}
def mean(xs):
xs = [x for x in xs if x is not None]
return statistics.mean(xs) if xs else None
def window_delta(b, a, w):
if not b or not a or b.get(w) is None or a.get(w) is None:
return None, "missing"
if b.get(w + "_resets_at") != a.get(w + "_resets_at"):
return None, "reset"
return round((a[w] - b[w]) * 100, 2), "ok"
scores = []
for p in sorted(glob.glob(os.path.join(RUNS, "*", "score.json"))):
s = json.load(open(p))
if s.get("config") not in CFGS:
continue
s["_dir"] = os.path.dirname(p)
s["_start"] = (s.get("meter_before") or {}).get("ts") or os.path.getmtime(os.path.join(s["_dir"], "stream.jsonl"))
s["_probe_tok"] = probe_tokens(s["_dir"])
scores.append(s)
# meter_before.ts is written at extraction time; order by stream file creation instead
for s in scores:
st = os.stat(os.path.join(s["_dir"], "stream.jsonl"))
s["_start"] = getattr(st, "st_birthtime", st.st_mtime)
scores.sort(key=lambda s: s["_start"])
cells, per_cfg = {}, {}
for s in scores:
cells.setdefault((s["config"], s["task"]), []).append(s)
per_cfg.setdefault(s["config"], []).append(s)
cell_rows = []
for (c, t), ss in sorted(cells.items(), key=lambda kv: (list(CFGS).index(kv[0][0]), TASKS.index(kv[0][1]) if kv[0][1] in TASKS else 99)):
row = {"config": c, "task": t, "n": len(ss),
"accept_rate": sum(1 for s in ss if s.get("accepted")) / len(ss),
"mean_turns": mean([s.get("num_turns") for s in ss]),
"mean_cost_usd": mean([s.get("total_cost_usd") for s in ss]),
"mean_wall_s": mean([s.get("wall_s") for s in ss]),
"mean_tokens": {k: mean([s["tokens"].get(k) for s in ss]) for k in W},
"mean_weighted_heuristic": mean([heuristic_run(s) for s in ss]),
"model_ok_all": all(s.get("model_ok") for s in ss)}
cell_rows.append(row)
# contiguous segments in global start order
segments, cur = [], None
for s in scores:
if cur and cur["config"] == s["config"]:
cur["runs"].append(s)
else:
cur = {"config": s["config"], "runs": [s]}
segments.append(cur)
cfg_rows = []
for c, ss in per_cfg.items():
tok_sum = {k: sum(s["tokens"].get(k) or 0 for s in ss) for k in W}
probe_sum = {k: sum(s["_probe_tok"].get(k, 0) for s in ss) for k in W}
raw_w, _ = weighted(tok_sum, TIER[c])
heur = sum(heuristic_run(s) for s in ss)
probe_raw, probe_heur = weighted(probe_sum, 1)
row = {"config": c, "runs": len(ss), "accept_rate": sum(1 for s in ss if s.get("accepted")) / len(ss),
"tokens_sum": tok_sum, "probe_tokens_sum": probe_sum,
"cost_sum_usd": round(sum(s.get("total_cost_usd") or 0 for s in ss), 4),
"weighted_raw_M": raw_w / 1e6, "heuristic_M": (heur + probe_heur) / 1e6}
for w in ("five_hour", "seven_day"):
per_run, excl = 0.0, 0
for s in ss:
d, why = window_delta(s.get("meter_before"), s.get("meter_after"), w)
if d is None:
excl += 1
else:
per_run += d
seg_sum, seg_n, seg_excl = 0.0, 0, 0
for g in segments:
if g["config"] != c:
continue
d, why = window_delta(g["runs"][0].get("meter_before"), g["runs"][-1].get("meter_after"), w)
if d is None:
seg_excl += 1
else:
seg_sum += d; seg_n += 1
row[w] = {"per_run_sum_pts": round(per_run, 2), "per_run_excluded": excl,
"segments_sum_pts": round(seg_sum, 2), "segments": seg_n, "segments_excluded": seg_excl,
"quantization_error_pts": f"+-{seg_n}",
"pts_per_M_heuristic": round(seg_sum / row["heuristic_M"], 4) if row["heuristic_M"] else None,
"pts_per_M_weighted_raw": round(seg_sum / row["weighted_raw_M"], 4) if row["weighted_raw_M"] else None}
cfg_rows.append(row)
cfg_rows.sort(key=lambda r: list(CFGS).index(r["config"]))
out = {"generated_from": len(scores), "cells": cell_rows, "configs": cfg_rows,
"segments": [{"config": g["config"], "runs": [s["run"] for s in g["runs"]],
"before": g["runs"][0].get("meter_before"), "after": g["runs"][-1].get("meter_after")} for g in segments],
"heuristic_weights": {"tokens": W, "tier": TIER}}
json.dump(out, open(os.path.join(HERE, OUTP + ".json"), "w"), indent=2, default=str)
f = lambda x, d=0: "-" if x is None else (f"{x:,.{d}f}")
md = ["# Results", "", f"{len(scores)} runs. Meter deltas in percentage points of the plan window (1-pt resolution).", "",
"## Per config x task", "",
"| config | task | n | accept | turns | input | cache read | cache create | output | cost $ | wall s | heuristic (M) | model ok |",
"|---|---|---|---|---|---|---|---|---|---|---|---|---|"]
for r in cell_rows:
t = r["mean_tokens"]
md.append(f"| {r['config']} | {r['task']} | {r['n']} | {r['accept_rate']:.0%} | {f(r['mean_turns'],1)} | {f(t['input_tokens'])} | "
f"{f(t['cache_read_input_tokens'])} | {f(t['cache_creation_input_tokens'])} | {f(t['output_tokens'])} | "
f"{f(r['mean_cost_usd'],4)} | {f(r['mean_wall_s'],1)} | {f(r['mean_weighted_heuristic']/1e6,2)} | {r['model_ok_all']} |")
md += ["", "## Per config: measured meter vs local heuristic", "",
"| config | runs | accept | cost $ | heuristic (M units) | 5h pts (segments) | 5h pts (per-run sum) | 7d pts (segments) | segments | 5h pts / M heuristic | 7d pts / M heuristic |",
"|---|---|---|---|---|---|---|---|---|---|---|"]
for r in cfg_rows:
fh, sd = r["five_hour"], r["seven_day"]
md.append(f"| {r['config']} | {r['runs']} | {r['accept_rate']:.0%} | {r['cost_sum_usd']} | {r['heuristic_M']:.2f} | "
f"{fh['segments_sum_pts']} (excl {fh['segments_excluded']}) | {fh['per_run_sum_pts']} | {sd['segments_sum_pts']} (excl {sd['segments_excluded']}) | "
f"{fh['segments']} | {fh['pts_per_M_heuristic']} | {sd['pts_per_M_heuristic']} |")
md += ["", "If the /usage heuristic matched the real meter, `pts / M heuristic` would be the same for every config.",
"Each segment carries up to +-1 pt of quantization error; trust a config's number only when its segment sum is several points.", ""]
open(os.path.join(HERE, OUTP + ".md"), "w").write("\n".join(md))
print("\n".join(md))
구성:
configs.json: 워커 넷, 전체 모델 id, low 블록용--effort low.configs.json
{
"haiku-med": {"args": ["--model", "claude-haiku-5-5"], "model": "claude-haiku-5-5", "tier": 1, "note": "default effort"},
"haiku-low": {"args": ["--model", "claude-haiku-5-5", "--effort", "low"], "model": "claude-haiku-5-5", "tier": 1},
"sonnet": {"args": ["--model", "claude-sonnet-5-5"], "model": "claude-sonnet-5-5", "tier": 3, "note": "default effort"},
"opus": {"args": ["--model", "claude-opus-5-5"], "model": "claude-opus-5-5", "tier": 5, "note": "default effort"}
}
- `tasks.json`: 작업별로 심어 둔 변경과 그 적용 방법. tasks.json
{
"explore": {"inject": null, "mode": null, "kind": "answer"},
"tests": {"inject": "inject/tests.patch", "mode": "commit", "kind": "code"},
"lint": {"inject": "inject/lint.patch", "mode": "commit", "kind": "code"},
"commit": {"inject": "reference/T1.patch", "mode": "stage", "kind": "answer"},
"refactor": {"inject": null, "mode": null, "kind": "code"},
"review": {"inject": "inject/review.patch", "mode": "worktree", "kind": "answer"},
"summary": {"inject": null, "mode": null, "kind": "answer"}
}
- `ruff.toml`: 린트 작업용으로 고정한 린트 설정. ruff.toml
# Pinned lint config for the `lint` task: a small explicit rule set.
# Upstream tinydb @ 18d73a1 is clean under it (the two per-file ignores cover
# pre-existing F401s: the __version__ re-export and middlewares' Optional).
[lint]
select = ["F401", "F541", "F841", "E722"]
[lint.per-file-ignores]
"tinydb/__init__.py" = ["F401"]
"tinydb/middlewares.py" = ["F401"]
심어 둔 변경:
inject/tests.patch:Query.__ge__의 비교를 뒤집어 테스트 하나가 실패하게 합니다.inject/tests.patch
diff --git a/tinydb/queries.py b/tinydb/queries.py
index cdab9fb..db00a2a 100644
--- a/tinydb/queries.py
+++ b/tinydb/queries.py
@@ -322,7 +322,7 @@ class Query(QueryInstance):
:param rhs: The value to compare against
"""
return self._generate_test(
- lambda value: value >= rhs,
+ lambda value: value > rhs,
('>=', self._path, rhs)
)
- `inject/lint.patch`: 고정된 린트 설정과 지적 사항 다섯 개를 추가합니다. inject/lint.patch
diff --git a/ruff.toml b/ruff.toml
new file mode 100644
--- /dev/null
+++ b/ruff.toml
@@ -0,0 +1,6 @@
+[lint]
+select = ["F401", "F541", "F841", "E722"]
+
+[lint.per-file-ignores]
+"tinydb/__init__.py" = ["F401"]
+"tinydb/middlewares.py" = ["F401"]
diff --git a/tinydb/operations.py b/tinydb/operations.py
index 7101a23..f8541fe 100644
--- a/tinydb/operations.py
+++ b/tinydb/operations.py
@@ -10,6 +10,7 @@ This would delete the ``foo`` field from all documents where ``foo`` equals 2.
from collections.abc import Callable, MutableMapping
+import sys
from typing import Any, Union
@@ -18,7 +19,7 @@ def delete(field: str) -> Callable[[MutableMapping], None]:
Delete a given field from the document.
"""
def transform(doc: MutableMapping):
- del doc[field]
+ removed = doc.pop(field)
return transform
diff --git a/tinydb/queries.py b/tinydb/queries.py
index cdab9fb..489a2be 100644
--- a/tinydb/queries.py
+++ b/tinydb/queries.py
@@ -227,7 +227,7 @@ class Query(QueryInstance):
:return: A :class:`~tinydb.queries.QueryInstance` object
"""
if not self._path and not allow_empty_path:
- raise ValueError('Query has no path')
+ raise ValueError(f'Query has no path')
def runner(value):
try:
diff --git a/tinydb/table.py b/tinydb/table.py
index f4027bf..eb8058c 100644
--- a/tinydb/table.py
+++ b/tinydb/table.py
@@ -818,7 +818,7 @@ class Table:
try:
raw_table = tables[self.name]
- except KeyError:
+ except:
# The table does not exist yet, so it is empty
raw_table = {}
diff --git a/tinydb/utils.py b/tinydb/utils.py
index 43cad4e..07114d5 100644
--- a/tinydb/utils.py
+++ b/tinydb/utils.py
@@ -2,6 +2,7 @@
Utility functions.
"""
+import os
from collections import OrderedDict, abc
from collections.abc import Iterator
from typing import TypeVar, Generic, Union, Optional, TYPE_CHECKING, cast
- `inject/review.patch`: 리뷰할 `if limit:` 경계 사례가 든 기능 diff. inject/review.patch
diff --git a/tinydb/table.py b/tinydb/table.py
index f4027bf..4e06ba1 100644
--- a/tinydb/table.py
+++ b/tinydb/table.py
@@ -244,10 +244,21 @@ class Table:
return list(iter(self))
- def search(self, cond: QueryLike) -> list[Document]:
+ def search(
+ self,
+ cond: QueryLike,
+ *,
+ sort_by: Optional[str] = None,
+ reverse: bool = False,
+ limit: Optional[int] = None,
+ ) -> list[Document]:
"""
Search for all documents matching a 'where' cond.
+ Results can optionally be sorted by a field (``sort_by``, descending
+ with ``reverse=True``; documents without the field always come last)
+ and truncated to the first ``limit`` results after sorting.
+
Note: a repeated search with the same ``cond`` is served from an
internal cache. The returned list is a fresh copy, but its
documents are the same objects held by the cache -- treat them as
@@ -255,8 +266,34 @@ class Table:
what a later identical search returns.
:param cond: the condition to check against
+ :param sort_by: optional field name to sort the results by
+ :param reverse: sort in descending order
+ :param limit: optional maximum number of results
:returns: list of matching documents
"""
+ if limit is not None and limit < 0:
+ raise ValueError('limit must not be negative')
+
+ # The cache always holds the plain, unsorted, untruncated result;
+ # _search returns a fresh list we are free to reorder/slice
+ docs = self._search(cond)
+
+ if sort_by is not None:
+ field = sort_by
+ present = [doc for doc in docs if field in doc]
+ missing = [doc for doc in docs if field not in doc]
+ present.sort(key=lambda doc: doc[field], reverse=reverse)
+ docs = present + missing
+
+ if limit:
+ docs = docs[:limit]
+
+ return docs
+
+ def _search(self, cond: QueryLike) -> list[Document]:
+ """
+ Plain search backed by the query cache. Returns a fresh list.
+ """
# First, we check the query cache to see if it has results for this
# query
프롬프트와 정답:
tasks/explore.md: 쿼리 캐시가 무효화되는 위치와 이를 일으키는 공개 메서드.tasks/explore.md
Where does TinyDB invalidate its query cache, and which public methods trigger it? Give file paths and function names.
- `tasks/tests.md`: pytest가 실패하면 라이브러리 버그를 고치고 테스트는 건드리지 않기. tasks/tests.md
pytest fails on this repo. Find the bug in the library code and fix it. Do not change the tests.
- `tasks/lint.md`: 동작을 바꾸지 않고 ruff 지적 사항을 전부 고치기. tasks/lint.md
ruff reports issues in tinydb/ (run `ruff check tinydb`). Fix them all without changing behaviour.
- `tasks/commit.md`: 스테이징된 변경의 커밋 메시지 작성. tasks/commit.md
Write the commit message for the staged changes. Output only the message.
- `tasks/refactor.md`: push와 pull 연산 기능. tasks/refactor.md
I keep writing little lambdas to append things to list fields, so can you add two update operations to `tinydb.operations`, next to `increment`, `delete` and friends? `push(field, value)` should append `value` to the list stored in `field`, keeping the existing order, and `pull(field, value)` should remove every occurrence of `value` from that list, not just the first one. If a document doesn't have the field yet, `push` should create it as a one-element list, while `pull` should just leave that document alone without raising. If the field exists but isn't a list, `push` has to raise a `TypeError` instead of doing something weird with it. They should be used exactly like the existing operations, e.g. `db.update(push('tags', 'python'), User.name == 'john')`.
- `tasks/review.md`: 커밋하지 않은 변경에서 버그와 경계 사례 리뷰. tasks/review.md
Review the uncommitted changes for bugs and edge cases. List findings only.
- `tasks/summary.md`: 레포의 구성을 150단어 이내로 요약. tasks/summary.md
Summarize how TinyDB is organised for a new contributor in at most 150 words: the main modules and what each owns.
- `tasks/explore.truth.md`: explore 검사의 정답과 매칭 규칙. tasks/explore.truth.md
# explore: ground truth (tinydb @ 18d73a1)
- The query cache is `Table._query_cache`, an `LRUCache` (tinydb/utils.py) created in
`Table.__init__` (tinydb/table.py, `query_cache_class = LRUCache`, capacity
`default_query_cache_capacity = 10`). It is filled in `Table.search`.
- Invalidation happens in one place: `Table._update_table()` (tinydb/table.py) ends with
`self.clear_cache()`, and `Table.clear_cache()` does `self._query_cache.clear()`.
- Every public write goes through `_update_table`, so these trigger it:
`insert`, `insert_multiple`, `update` (all three branches: callable/fields, by doc_ids, by cond),
`update_multiple`, `upsert` (via `update` or `insert`), `remove` (by cond or doc_ids), `truncate`.
`Table.__init__(persist_empty=True)` also calls it. `clear_cache()` itself is public.
`TinyDB` (tinydb/database.py) forwards these via `__getattr__` to the default table;
`drop_table`/`drop_tables` do not clear a cache, they forget the `Table` instances (`self._tables`).
## Check rule (check.py explore)
PASS iff the final answer
1. contains `table.py`, AND
2. contains `clear_cache` or `_query_cache`, AND
3. names at least 3 distinct methods among
insert, insert_multiple, update, update_multiple, upsert, remove, truncate (word match, case-insensitive).
tasks/explore.truth.md on GitHub
- `tasks/review.truth.md`: review 검사의 정답과 매칭 규칙.tasks/review.truth.md
# review: planted defect and matching rule
The uncommitted change is the reference T4 patch (`Table.search(cond, *, sort_by, reverse, limit)`)
with one defect: the truncation guard is `if limit:` instead of `if limit is not None:`, so
`search(cond, limit=0)` returns ALL matching documents instead of `[]` (0 is falsy).
Everything else in the change is correct (missing-field docs last in both directions,
negative limit -> ValueError, cache keeps the plain result).
## Check rule (check.py review)
PASS iff the final answer (case-insensitive) matches at least one of:
- `limit\s*=?=\s*0` (e.g. "limit=0", "limit == 0")
- `limit\s+(of\s+)?(0|zero)\b` (e.g. "limit of 0", "limit zero")
- `\b(0|zero)\s+limit` / `zero[- ]limit`
- `limit` within 150 characters of `falsy|falsey|truthy|truthiness` (either order)
- `if limit:` together with `0` or `zero` within 150 characters
Decoy answers (generic review, or only the sort order) must FAIL.
tasks/review.truth.md on GitHub
월요일에 할 일
- Claude Code 2.1.257 이상을 쓰십시오. force 플래그가 있어야 하고,
/tasks가 각 서브에이전트의 모델을 보여 주려면 2.1.243 이상이어야 합니다. - 어느 파일을 쓸지 정하십시오. 탐색기만이면 Explore 파일, 모든 서브에이전트면 env 블록입니다.
-
Explore.md를.claude/agents/Explore.md로 저장하거나, env 블록을.claude/settings.json에 병합하십시오. - 특히 Bedrock, Vertex, Foundry에서는
haiku별칭이 아니라 전체 idclaude-haiku-5-5를 쓰십시오. - 세션을 시작해 Explore 에이전트로 코드베이스 검색을 요청한 뒤,
/tasks를 열어 돈 모델과 effort를 읽으십시오. - 복잡한 에이전트형 코딩을 하는 서브에이전트는 Sonnet 5.5나 Opus 5.5에 두고, 그런 에이전트를 일부러 지정했다면 FORCE는 설정하지 마십시오.
- 우리 표를 자기 레포에서 믿기 전에 자기 작업 일곱 개를 하네스로 다시 돌려 보십시오.
-
/usage는 할당량 예측이 아니라 "어떤 스킬이나 에이전트가 내 로컬 기록을 많이 차지하는가"로 읽으십시오.
측정값
각 헤드리스 실행의 rate_limit_event에서 읽은 플랜 미터이며 해상도는 1포인트입니다. 각 블록은 연속이었고 계정에 다른 것이 손대지 않았습니다. 주간 창은 182번 실행 모두에서 67%였습니다.
| 블록 | 실행 | 5시간 이전 | 5시간 이후 | 주간 이전 | 주간 이후 | 정가 |
|---|---|---|---|---|---|---|
| Haiku 5.5 low | 70 | 21% | 21% | 67% | 67% | $0.2382 |
| Haiku 5.5 medium | 70 | 21% | 21% | 67% | 67% | $0.3006 |
| Sonnet 5.5 | 21 | 21% | 22% | 67% | 67% | $1.2339 |
| Opus 5.5 | 21 | 22% | 23% | 67% | 67% | $2.3175 |
매트릭스 요약: 182번 실행, 전체 매트릭스는 정가로 약 $4.09, 5시간 창 2포인트, 주간 0포인트. 각 블록에는 최대 1포인트의 반올림이 들어 있어서 Sonnet의 1포인트와 Opus의 1포인트는 구분할 수 없습니다. 범위가 정해진 작업은 어느 모델이든 이 미터에서는 보이지 않습니다. 세 모델의 턴당 상대 비용에 대한 지원 페이지는 배수를 제시하지 않습니다 s9.
로컬 분석과 비교한 구성별 표:
| 구성 | 실행 | 통과율 | 정가 | /usage 휴리스틱 (M 단위) |
5시간 포인트 |
|---|---|---|---|---|---|
| haiku-med | 70 | 100% | $0.3006 | 29.48 | 0.0 |
| haiku-low | 70 | 100% | $0.2382 | 23.91 | 0.0 |
| sonnet | 21 | 100% | $1.2339 | 16.15 | 1.0 |
| opus | 21 | 100% | $2.3175 | 27.30 | 1.0 |
/usage 분석은 이 컴퓨터의 로컬 기록으로 계산됩니다 s3. 클라이언트 바이너리에서 읽은 공식은 모델 티어에 레코드 가중치를 곱한 것입니다. 모델 티어: Fable 10, Opus 5, Haiku 1, 그 외 3. 레코드 가중치: 캐시된 토큰 1, 캐시되지 않은 입력 10, 캐시 생성 12.5, 출력 50. 이는 클라이언트의 로컬 귀속 가중치이며 청구가 아니고, 서버가 할당량을 어떻게 세는지의 증거도 아닙니다. 그 차이가 드러납니다. Haiku medium 블록은 29.48, Opus 블록은 27.30이어서 패널은 Haiku 블록을 Opus 블록 위에 놓지만, 실제 미터는 0포인트 대 1포인트였습니다.
한 계정의 한 달 치 로그가 토큰을 둔 곳: 서브에이전트 실행 2,631번, 그중 42번이 Haiku 4.5, 서브에이전트가 전체 토큰의 48.1%, 가중 비용에서 Haiku 4.5가 0.27%, 모델과 무관하게 서브에이전트 런치 프롬프트 중앙값 47,117 토큰.
같은 통과 기능(refactor)에서 effort가 식욕을 바꿉니다. Haiku medium은 13.7턴에 걸쳐 캐시 읽기 토큰 232,985개를, Haiku low는 7.6턴에 걸쳐 139,801개를, Opus는 8.3턴에 걸쳐 87,612개를 읽었습니다.
더 알아보기
- 100k 가격 절벽: 프롬프트 토큰이 100,000을 넘으면 초과분이 아니라 요청 전체에 높은 티어가 적용됩니다 s8. 캐시 토큰이 100k에 포함되는지는 그 저자의 해석이지 인용된 규칙이 아닙니다. 구독의 미터가 아니라 API 키 경제에 해당하는 이야기입니다. 가격 페이지: s20.
- 토크나이저: 같은 텍스트가 Haiku 5.5에서 Haiku 4.5보다 약 30% 많은 토큰으로 세어지며, API에서는 adaptive thinking이 기본으로 켜져 있고 effort가 레버입니다 s12.
- effort와 작업당 토큰: 독립 측정 결과 Intelligence Index 작업당 출력이 max effort에서 약 162k 토큰, high effort에서 약 55k 토큰이었고 점수는 38이었습니다 s7.
- 수치와, 절감이 작은 메인 컨텍스트에서 나온다는 본인의 단서가 함께 담긴 실무자의 전체 구성: s4.
- 열린 이슈 #100082, 일부 빌드에서 서브에이전트 frontmatter 모델이 무시됨: s10.
- 열린 이슈 #100154, 서브에이전트 트랜스크립트가
output_tokens를 과소 보고함. 같은 브리프에서 461 대 15,575이고 서버 측 청구는 영향이 없습니다 s11./usage는 로컬 기록의 출력에 50을 곱하므로 이 과소 집계가 패널에 닿습니다. - 여기서 테스트하지 않은 것: 메인 모델로서의 Haiku(출처들은 Haiku 5.5를 서브에이전트로 규정합니다), 게이트웨이를 통한 공급자 간 모델 혼합(Claude Code 설정이 아님), 1M 컨텍스트 프롬프트, Max 플랜에 포함된 API 크레딧(Claude Code 할당량이 아니라 Claude Platform).
자주 묻는 질문
CLAUDE_CODE_SUBAGENT_MODEL을 설정하면 Explore가 Haiku로 갑니까?
아닙니다. 그것만으로는 내장 Explore와 Plan 서브에이전트가 바뀌지 않습니다 s2. Explore 에이전트 파일이나 force 플래그를 더하십시오.
FORCE가 내 Opus 서브에이전트를 망가뜨립니까?
모든 서브에이전트에 모델 하나를 적용하므로 그렇습니다. 일부러 Opus로 지정한 에이전트도 Haiku에서 돕니다. 탐색기만 저렴하게 하려면 Explore 파일을 쓰십시오.
Haiku 5.5는 서브에이전트 작업에 충분합니까?
작은 레포 하나의 범위가 정해진 이 일곱 작업에서는 low effort에서 70번 중 70번, medium에서 70번 중 70번 통과했습니다. Anthropic 자신도 복잡한 에이전트형 코딩은 Sonnet 5.5와 Opus 5.5에 맡기며 s1, Terminal-Bench 4.0의 격차는 39.2% 대 70.6%입니다.
전환하면 플랜 할당량을 얼마나 돌려받습니까?
범위가 정해진 작업이라면 어느 모델이든 미터 1포인트 미만입니다. 182번짜리 매트릭스 전체가 5시간 창을 2포인트, 주간 창을 0포인트 움직였습니다. 진짜 한 주가 가는 곳은 메인 컨텍스트와 각 서브에이전트가 달고 다니는 런치 프롬프트입니다.
더 아끼려고 thinking을 끌 수 있습니까?
Claude Code에서 이 모델들에서는 안 됩니다 s21. 남아 있는 다이얼은 effort입니다.
AIDive