For AI agentsA Markdown index of these docs is at /llms.txt. Every page is also Markdown: add .md to its address.

Agent Rules / Agent workflow

Is a list of callers ever the full list?

Rule AR-27

Treat \"callers found\" as the full list only when the tool says the list is full.

Every caller search returns a list. Few of them say whether it is the whole list. A list that doesn't say is a guess with good formatting: it may be the full list, and nothing in it tells you. An agent should treat the entries as lower bounds and write down what the tool could not rule out.

Why it matters

rg -n "send_invoice" returns five lines. The agent changes the function, updates five calls, and moves on. The sixth caller is:

pythonhandler = getattr(mailer, f"send_{doc_type}") handler(doc)

Text search had no way to match it. A language-server reference search returned the same five, because the sixth has no reference to resolve. Both tools answered the question they could answer. Neither said "and there may be calls I can't see". The agent read five results as "five callers".

The same shape appears with other tools:

  • A references request resolves what the server's type information resolves. Where a value's type is unknown, the call isn't listed, and the list doesn't say so.
  • A text search finds spellings. It can't find a name that is built or imported under another name.
  • A test run exercises the paths the tests take.

A full list is possible in a closed world (one file, no dynamic access). In real code you need a statement from the tool, or your own bound.

The steps

  1. Ask what the tool's list covers. Does it say the list is full? Does it report what it could not resolve? If the answer is no, the list is a lower bound.
  2. Write the bound next to the number. "5 callers found by rg; dynamic sites not searched" is a claim you can stand behind. "5 callers" is not.
  3. Search for what the list can't hold (AR-25 step 4): built names, tables, aliases, wildcard re-exports.
  4. Count the open items separately. Sites you looked at and read are checked. Sites you saw and did not read are open. Sites a tool said it couldn't resolve are open.
  5. Report the split in the final message (AR-30): checked, open, and why.
  6. Don't convert absence into proof. "No other callers found" is a statement about the search. "No other callers exist" needs a tool that says the list is full or a reason the closed world holds.

Worked example

Two ways to write the same finding after changing send_invoice(doc) to send_invoice(doc, locale):

weak: Found 5 callers and updated all of them. better: Updated 5 direct calls (rg and find-references agree). Not covered: mailer.py:88 builds the name with getattr(mailer, f"send_{doc_type}"). I read it: for doc_type == "invoice" it calls send_invoice(doc) with one argument. I did not run it; no test reaches that line.

Both reports are about the same work. The second one lets the next reader, or the next agent, decide where to look.

Rule block for AGENTS.md / CLAUDE.md

markdown## Caller lists - A list of callers is full only if the tool says so. Otherwise it is a lower bound. - Say what each search could not see: built names, tables of functions, aliases, wildcard re-exports, code outside this repo. - Write "N found, M open" instead of "N callers". Open means seen but not read, or reported unresolved. - Do not turn "no other callers found" into "there are no other callers". - Open items go in the final message with file and line.

Edge cases

  • Closed worlds. A helper used inside one module only with no __all__, no dynamic access in the file, and no re-export is a place where a text search is close to the full list. Say that reason in a clause; don't assume it.
  • Tools that do report completeness. Use the statement. Quote what it says and what it left out.
  • Test-only callers. Count them; they are callers. Mocks that patch the name by string ("billing.money.format_amount") are another string-keyed reference to search for.
  • Entry points registered outside code (config, pyproject.toml scripts, package.json bin fields, route files) call functions by name from data. Search the config files too.
  • A list of zero. "Zero callers" is the riskiest sentence in a code change, since deleting the function follows from it. Require both a tool statement and a dynamic-site search first.

What LLMGRAPH shows

Each caller prints certain or open. A getattr call written on one line prints 'plus 1 dynamic call nearby that could reach anything' with its file. A getattr result held in a variable and called on the next line is not listed.

References

  • LSP 3.17 specification, Find References request: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/. Verified 2026-10-30: the request is sent "to resolve project-wide references" for the symbol at a position and returns Location[] | null.
  • Python library reference, getattr: https://docs.python.org/3/library/functions.html
  • Pyright type inference (unknown types in partially typed code): https://github.com/microsoft/pyright/blob/main/docs/type-inference.md. Verified 2026-10-30: when pyright cannot infer a type it sets it to Unknown, "a special form of Any", which can leave blind spots; for a function with all parameters unannotated it infers the return type per call from the argument types.

Changelog

  • 2026-10-30: draft 1.
  • 2026-10-30: docs checks resolved
  • 2026-10-30: copy-gate wording fixes
  • AR-25 What to check before changing a function signature?
  • AR-26 How does an agent know an edit is finished?
  • AR-30 What should an agent report when it can't verify an edit?
  • AR-07 Why does my coding agent miss calls made through getattr?
  • AR-01 Why do star imports make my coding agent miss callers?

Last verified

For agentsthis page as Markdown/agent-readable/ar-27.mdllms.txt