scripts/ci/check_agents_md_size.py (in scripts/check and lint.yml): root <= 12k, every AGENTS.md plus its ancestors <= 30k (the joined root->cwd block on a 128k window, minus the per-file labels), every area file <= _MAX_HINT_CHARS read from the source so the two cannot drift. On origin/main it reports the root and all 11 root-to-area chains over.
66 lines
3.0 KiB
Python
66 lines
3.0 KiB
Python
#!/usr/bin/env python3
|
|
"""Fail when a tracked AGENTS.md would be truncated when an agent loads it.
|
|
|
|
An agent started in a directory gets every AGENTS.md from the git root down to that directory as
|
|
ONE project-context block: ``agent/prompt_builder.py`` joins the chain before truncating it, at 6%
|
|
of the context window (30,720 chars on a 128k-token model, a 20k floor on small ones). So the
|
|
budget is per CHAIN: each AGENTS.md plus all of its ancestors stays within CHAIN_MAX_CHARS (the
|
|
128k budget minus room for the per-file provenance labels), and the root, which is in every
|
|
chain, stays within ROOT_MAX_CHARS so area files have room. An area file reached as a
|
|
subdirectory hint is also capped by ``_MAX_HINT_CHARS`` in ``agent/subdirectory_hints.py``, read
|
|
from the source here so the two can never drift. Shrink a file (move long form to the developer
|
|
guide or a sibling doc) instead of raising any number.
|
|
|
|
Exit codes: 0 = all within their caps, 1 = a file or chain is over (paths and sizes printed).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import ast
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path, PurePosixPath
|
|
|
|
ROOT_MAX_CHARS = 12_000
|
|
CHAIN_MAX_CHARS = 30_000
|
|
|
|
|
|
def _int_constant(source: Path, name: str) -> int:
|
|
for node in ast.walk(ast.parse(source.read_text(encoding="utf-8-sig"))):
|
|
if isinstance(node, ast.Assign) and any(getattr(t, "id", None) == name for t in node.targets):
|
|
return int(ast.literal_eval(node.value))
|
|
raise SystemExit(f"{source}: no {name} assignment; update scripts/ci/check_agents_md_size.py")
|
|
|
|
|
|
def _chain(rel: str, sizes: dict[str, int]) -> list[str]:
|
|
"""Every tracked AGENTS.md from the root down to ``rel``, root first."""
|
|
ancestors = [str(p / "AGENTS.md") if str(p) != "." else "AGENTS.md" for p in PurePosixPath(rel).parents]
|
|
return [p for p in reversed(ancestors) if p in sizes and p != rel] + [rel]
|
|
|
|
|
|
def main(repo: Path) -> int:
|
|
hint_cap = _int_constant(repo / "agent" / "subdirectory_hints.py", "_MAX_HINT_CHARS")
|
|
tracked = subprocess.run(
|
|
["git", "ls-files", "-z", "--", "AGENTS.md", "*/AGENTS.md"], cwd=repo, capture_output=True,
|
|
check=True, stdin=subprocess.DEVNULL, timeout=60,
|
|
).stdout.decode("utf-8").split("\0")
|
|
sizes = {rel: len((repo / rel).read_text(encoding="utf-8-sig")) for rel in filter(None, tracked)}
|
|
over = []
|
|
for rel, size in sorted(sizes.items()):
|
|
cap = ROOT_MAX_CHARS if rel == "AGENTS.md" else hint_cap
|
|
if size > cap:
|
|
over.append(f"{rel}: {size} chars > {cap} cap")
|
|
chain = _chain(rel, sizes)
|
|
total = sum(sizes[p] for p in chain)
|
|
if len(chain) > 1 and total > CHAIN_MAX_CHARS:
|
|
over.append(f"{' + '.join(chain)}: {total} chars > {CHAIN_MAX_CHARS} chain cap")
|
|
for line in over:
|
|
print(line)
|
|
if over:
|
|
print("Move long form into website/docs/developer-guide/ or a sibling doc instead of raising a cap.")
|
|
return 1 if over else 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main(Path(sys.argv[1]) if len(sys.argv) > 1 else Path(__file__).resolve().parents[2]))
|