I was reading the 2.17.0 changelog late one evening when a single line stopped me: "Rules no longer crowd out everything else." Read the other way round, that sentence says rules had been crowding out everything else until now.
As an indie developer I run four Lab sites, and their operating rules have been split and trimmed more than once, yet the total kept growing. It was hard not to picture that thick rule set quietly pushing my skills and MCP tools out of the agent's context without my noticing. My stomach sank a little.
So before installing the update, I decided to count. The result was 8,613 tokens over the budget.
The two limits that 2.17.0 settles
The rules documentation describes two limits, and they use different units, which matters more than it sounds.
| Limit | How it is measured | What happens when you exceed it |
|---|---|---|
| 24,000 bytes per file | Byte size after expanding @[label](path) includes | The tail of the file is truncated |
| 20,000 tokens in total | All global and always_on rules combined | The largest files are demoted, one by one, to a pointer of path plus description |
That 20,000-token budget is separate from the customization budget that skills, subagents, and MCP tools draw from. Before 2.17.0 they all shared one pool, so a heavy rule set meant a lighter everything else.
One more detail is worth holding onto. A model_decision rule enters the prompt as a pointer from the start: only its path and description are shown up front, and the agent opens the full text when it judges the task relevant. From the agent's point of view, a rule you demote yourself and a rule the budget demotes for you look almost identical.
A small tool to count what you have
I wanted three answers: whether any file exceeds 24,000 bytes after includes are expanded, whether the always_on total fits under 20,000 tokens, and, if not, which files would fall to pointers first.
Antigravity will tell you all of this, but only after you upgrade and open the workspace. I wanted to know beforehand, so I wrote a short Python script that reads a workspace's rule files and runs the estimate.
#!/usr/bin/env python3
"""Estimate Antigravity's rule budgets for a workspace before upgrading.
- warn on files over 24,000 bytes (after expanding @[label](path) includes)
- compare the always_on total (plus frontmatter-less AGENTS.md / GEMINI.md) with 20,000 tokens
- if over budget, simulate demotion largest-first and show what stays inline
Token counts use tiktoken cl100k_base as a proxy; Gemini's real count will differ a little.
"""
import re, sys, pathlib
import tiktoken
FILE_LIMIT_BYTES = 24_000
RULES_BUDGET_TOKENS = 20_000
VALID_TRIGGERS = {"always_on", "model_decision", "glob", "manual"}
enc = tiktoken.get_encoding("cl100k_base")
INCLUDE = re.compile(r"@\[[^\]]*\]\(([^)]+)\)")
def expand_includes(text: str, base: pathlib.Path) -> str:
def repl(m):
target = pathlib.Path(m.group(1)).expanduser()
if not target.is_absolute():
target = (base / target).resolve()
try:
body = target.read_text(encoding="utf-8")
except OSError:
return m.group(0) # leave the reference as-is when the file is missing
return re.sub(r"\A---\n.*?\n---\n", "", body, flags=re.S) # drop the included file's frontmatter
return INCLUDE.sub(repl, text)
def read_rule(path: pathlib.Path):
raw = path.read_text(encoding="utf-8")
trigger, desc = None, ""
m = re.match(r"\A---\n(.*?)\n---\n", raw, flags=re.S)
if m:
fm = m.group(1)
t = re.search(r"^trigger:\s*(\S+)", fm, flags=re.M)
d = re.search(r'^description:\s*"?(.*?)"?\s*$', fm, flags=re.M)
trigger = t.group(1) if t else "INVALID"
if trigger not in VALID_TRIGGERS:
trigger = "INVALID" # alwaysOn / modelDecision etc. are silently discarded
desc = d.group(1) if d else ""
elif path.name in ("AGENTS.md", "GEMINI.md"):
trigger = "always_on" # always active without frontmatter
else:
trigger = "INVALID" # a rules/ file without frontmatter is silently discarded
body = expand_includes(raw, path.parent)
return {"path": path, "trigger": trigger, "desc": desc,
"bytes": len(body.encode("utf-8")), "tokens": len(enc.encode(body))}
def main(root: str):
root = pathlib.Path(root)
files = [p for p in (root / "AGENTS.md", root / "GEMINI.md") if p.exists()]
files += sorted((root / ".agents" / "rules").glob("*.md"))
rules = [read_rule(p) for p in files]
active = [r for r in rules if r["trigger"] == "always_on"]
total = sum(r["tokens"] for r in active)
print(f"{'file':<28}{'trigger':<16}{'bytes':>8}{'tokens':>8} note")
for r in rules:
note = []
if r["bytes"] > FILE_LIMIT_BYTES: note.append("OVER 24,000 B -> truncated")
if r["trigger"] == "INVALID": note.append("discarded (no/invalid frontmatter)")
print(f"{r['path'].name:<28}{r['trigger']:<16}{r['bytes']:>8}{r['tokens']:>8} {' / '.join(note)}")
print(f"\nalways_on total: {total:,} tokens / budget {RULES_BUDGET_TOKENS:,}")
if total <= RULES_BUDGET_TOKENS:
print("OK: everything stays inline"); return
demoted, remaining = [], total
for r in sorted(active, key=lambda r: r["tokens"], reverse=True):
if remaining <= RULES_BUDGET_TOKENS: break
demoted.append(r); remaining -= r["tokens"]
print(f"over by {total - RULES_BUDGET_TOKENS:,}; demoted to pointers (largest first):")
for r in demoted:
print(f" - {r['path'].relative_to(root)}: {r['desc'] or '(no description)'}")
print(f"inline after demotion: {remaining:,} tokens")
if __name__ == "__main__":
main(sys.argv[1] if len(sys.argv) > 1 else ".")Run pip install tiktoken once, then python3 rules_budget_probe.py . from the workspace root. The output is one line per rule file with its trigger, byte size, and token estimate, followed by the always_on total and, when you are over, the list of files that would become pointers. Global rules under ~/.gemini/config/rules/ share the same budget, so if you keep any there, add that directory to the list the script scans.
Two choices in the script deserve a word. Include expansion happens before the size check because the documentation states that the 24,000-byte limit is measured after @[label](path) includes are expanded. And trigger is checked against the four valid values because a misspelled trigger does not raise an error; the rule is simply dropped, which I ran into myself a few minutes later.
The token count is a cl100k_base approximation, so it will not match Gemini's tokenizer exactly. It is not the tool for a decision that hinges on a few hundred tokens, but it answered the question I had: are we over, and by roughly how much.
What the count showed
My Lab operating rules consist of an AGENTS.md with the essentials, plus five companion files: a reference table, shared procedures, site-specific values, the writing voice rules, and a checklist. Here is what the script reported with all of them set to always_on.
| File | Role | Bytes | Tokens (approx.) |
|---|---|---|---|
| AGENTS.md | Operating essentials | 15,392 | 5,685 |
| content-core.md | Shared procedures and checks | 21,993 | 7,988 |
| reference.md | Reference tables | 19,944 | 7,114 |
| voice.md | Writing voice rules | 12,280 | 4,593 |
| site.md | Site-specific values | 5,166 | 1,830 |
| checklist.md | Writing checklist | 3,623 | 1,403 |
| always_on total | 28,613 | ||
The total came to 28,613 tokens, 8,613 over the budget. In the simulation, content-core.md (7,988) and reference.md (7,114) would be demoted largest-first, leaving 13,511 tokens of rules inline.
That is where I paused. Of the two files that would be demoted, content-core.md was the one I most wanted the agent to have in front of it at all times. Meanwhile the 3,623-byte checklist would stay inline without my lifting a finger.
The budget does not weigh importance. It weighs size. The documents I had written most thoroughly because they mattered most were, for exactly that reason, the biggest, and the biggest are what fall first. The reason I made them thick and the reason they get demoted turned out to be the same reason.
Three things that ran against my intuition
Running the estimate a few times surfaced three things I would not have learned from the documentation alone.
The first is the largest-first order itself. If you need to shrink something, trimming your most important document is what moves the needle most.
The second is that you can be comfortably under the token budget and still be truncated by bytes. When I appended a single line, @[reference](./reference.md), to the end of AGENTS.md to pull the reference table in, the expanded file came to 35,231 bytes, past the 24,000 limit and into truncation territory. Its token count was 12,769, well inside the 20,000 budget. Includes are convenient, but everything they pull in counts toward that file's byte size.
The third is that a test file with trigger: alwaysOn vanished from the listing without a warning. The documentation says such rules are silently discarded, and it means it. My own script initially printed alwaysOn as if it were fine; only after I added the validation did it report INVALID. When a rule seems not to be taking effect, the first suspect is spelling, much like the cases in Four Reasons Your .antigravityignore Rules Are Not Taking Effect.
Demote it yourself before the budget does
Having seen the numbers, I switched the two files to model_decision myself. All that took was editing the frontmatter of content-core.md and reference.md and rewriting each description into one sentence that says when the file should be opened.
Counting again, the always_on total dropped to 13,511 tokens, with 6,489 to spare. What the agent sees is nearly the same as if the budget had demoted them. The one difference is that I chose which files went, rather than the budget choosing by size.
Decide the demotion order yourself before the budget decides it for you. That is the one line I now hold to every time the rule set grows.
Writing those descriptions was the part that took real thought. A description like "shared procedures" tells the agent nothing about when to open the file; "open before running the content checks or pushing to the repository" does. The pointer is only as useful as the sentence next to it.
I also set a rule for what stays. Short prohibitions and the writing voice remain always_on; long procedures and reference tables get a careful description and move to model_decision. Procedures have a clear moment when they are needed, and the agent opening them at that moment is enough.
The same 2.17.0 release moved per-repository configuration to .gemini/config.json and stopped reading the old .agents/settings.json. If you keep a watcher like the one in A Small Guard Script That Catches an Unattended Agent Rewriting Its Own Settings, this is a good moment to point it at the new location as well.
Count one workspace first
Before you upgrade, I'd suggest running the script once from your workspace root. If the total is over 20,000, rewrite the description of the single largest file and move it to model_decision. That alone turns a demotion the budget would have picked into one you picked.
I may be missing something, but demoting the document I cared about most turned out to be fine: the agent opens it when the task calls for it, and it has not once needed me to remind it.