Show HN: Ambits – agentic grep/rg tool will history tracking A developer released Ambits, an open-source code-reading tool for AI coding agents that returns a symbol's definition as structured JSON and records every symbol an agent reads so the history can be restored after a context window compaction. Ambits ships as two front ends over one engine — `ambits rg` and `ambits grep` — with `ambits rg` matching ripgrep's flags and regex crate, and it is currently built and tested against Claude Code, installed via `cargo install ambits`. The tool addresses agents re-reading whole files and losing context on compaction by letting them request a symbol such as `App/process_compaction` instead of 2,000 lines of `app.rs`, and it can hand that read history back automatically after every compaction via `ambits hook install --project .`. A code-reading tool for AI agents, and a memory of what they have read. Coding agents read whole files to find one function, re-read code they already know, and lose all of it the moment the context window compacts. ambits addresses both halves of that: - Reading — ambits show returns a symbol's definition as structured JSON, addressed by name or by content hash. The agent asks for App/process compaction , not for 2,000 lines of app.rs . - Remembering — ambits records every symbol the agent reads, at what depth, throughout the session. After a compaction it can hand that history back, so the agent knows what it already understands instead of rediscovering it. Both surfaces are plain text and JSON with no vendor coupling, so the record can be handed between agents, or between providers. Ingestion is currently built and tested against Claude Code. There is also a live TUI, for when you want to watch what your agent is actually looking at. cargo install ambits Read a symbol instead of a file ambits -p . show 'src/app.rs::App/process compaction' What has this session read so far? ambits -p . restore-context Hand that history back automatically after every compaction ambits hook install --project . Watch it live ambits -p . A grep whose every hit knows which symbol it landed in. ambits -p . rg 'is binary' every use and definition ambits -p . rg 'fn enclosing' -t rust one file type ambits -p . rg 'TODO' -g ' tests/ ' globs; excludes ambits -p . rg 'Journal::open' -A 3 with trailing context ambits -p . rg 'unwrap\ \ ' -c matching lines per file js src/find.rs:67:7: full BINARY SNIFF BYTES const BINARY SNIFF BYTES: usize = 8 1024; src/find.rs:337:4: full is binary fn is binary buf: & u8 - bool { src/find.rs:338:21: full is binary buf.iter .take BINARY SNIFF BYTES .any |&b| b == 0 src/find.rs:442:8: full search file if is binary &buf || matcher.worth searching &buf { file:line:column: — the prefix every grep consumer already parses — then the symbol the match sits in and how deeply this session has read it, then the line. --no-symbol drops that field for output byte-identical to ripgrep's. grep 1 and ripgrep assign opposite meanings to the same short flags , so no single command can be faithful to both: | Flag | GNU grep | ripgrep | |---|---|---| | -L | --files-without-match | --follow symlinks | | -z | --null-data | --search-zip | | -r | --recursive | --replace | | -h | --no-filename | help | ambits rg and ambits grep are two front ends over one engine — the same matcher, the same symbol attribution, the same output contract — each faithful to the tool it is named after. rg is the one to reach for: Claude Code's own Grep tool is ripgrep-backed, so it is the dialect agents already speak. ambits grep keeps grep's defaults rather than ours: line numbers are opt-in, there is no column, and -h is --no-filename . Three flags it cannot honour say so rather than pretending — -P no lookaround in this engine, so a PCRE pattern would match something other than what it says and -z NUL-separated input would change what a line is are refused; -r / -R are accepted no-ops, since the search is always recursive. The flags are ripgrep's, down to the regex engine: -i -w -x -F -v -U -e -g -t -A -B -C -n -N -o -l -c -m -M -q --hidden --no-ignore --heading --color --json . That is not imitation for its own sake — Claude Code's Grep tool is ripgrep-backed, so an agent reaching for this already knows the dialect, and the same regex crate means patterns behave identically, including the shared absence of backreferences and lookaround. | Form | Meaning | |---|---| | full name , signature name , … | This session has read the symbol, at that depth | | — name | It has not | | name | No coverage journal: unknown , which is not the same as unread | | - | The match is not inside any symbol — a use line, or a file no parser handles | The third row is the one that matters. An empty column would read as "unread" when the honest answer is "nobody was watching", and only one of those means go read it. In --json , a coverage object on the summary event is what distinguishes them. Every text file is searched, not only the parseable ones — a hit in a TOML file is a real hit, it simply has no symbol. A search prints source into an agent's context, so it records what it showed: every symbol whose matching line was printed is journaled as read, at the hash it was searched at. Modes that print no source — -q , -l , -c — record nothing, and neither do matches cut past --head-limit . The journal is a record of what was seen , not of what the process computed. Output is always sorted by path, line and column, because determinism is worth more to an agent than the microseconds. --head-limit caps at 200 matches and -M clips lines at 300 columns, because this output lands in a context window rather than a terminal; 0 lifts either. --column is on by default, because it is what disambiguates two matches on one line. Exit codes are grep's: 0 matched, 1 nothing matched, 2 error. Every other command scans the project first: walk, parse every file, then answer. A search inverts that — walk, read, reject on the raw bytes, and parse only the survivors. Searching this repo for classify reads 61 files and parses the 5 that matched, in about 0.01s; a pattern that matches nothing parses nothing and costs 0.00s, where a symbol-index search would pay for a full parse every time. ambits -p . callers centered rect centered rect — 2 call sites in 2 callers src/ui/alignment.rs::render src/ui/alignment.rs:16 src/ui/compaction.rs::render src/ui/compaction.rs:15 Call sites come from the grammar's own tags query, so a mention in a comment or inside a string literal is never reported — the answer is a call node or it is not there. Each site is attributed to the innermost symbol containing it, and that id goes straight into show . Matching is by name, not by resolution. tree-sitter parses; it does not do type inference, so a call to new cannot be tied to one of the twelve definitions named new . On this repo 895 of 993 function names are unique, so most answers are exact — but callers new returns every call to anything named new . --format json sets name matched only: true so a consumer cannot mistake this for a resolved call graph. References are extracted on demand rather than stored, so rg , show , and the TUI pay nothing for this. It costs about 0.06s on this repo against under 0.01s for a search — and unlike one, it reports call nodes only, so the definition and the doc comments mentioning it do not come back with them. ambits -p . show 'src/fmt.rs::tokens' {"schema version":2,"coverage":{"session id":"30172621-…","symbols read":1034}, "results": {"query":"src/fmt.rs::tokens","selector":"id", "matches": {"id":"src/fmt.rs::tokens","name":"tokens","file":"src/fmt.rs", "lines": 10,18 ,"bytes": 418,641 ,"content hash":"b3:46d7bd8b…","label":"fn", "estimated tokens":88,"definition":"pub fn tokens n: u64 - String {\n …", "read depth":"full"} } } A selector is either a symbol id —