feat(agent): materialize a project map at task start - #3404
feat(agent): materialize a project map at task start#3404kovtcharov-amd wants to merge 2 commits into
Conversation
The flagship opened every task blind — it guessed directory names, guessed which commands existed, and guessed which shell it was talking to, and each wrong guess cost a full round trip to learn something one orientation pass establishes once. It now gets a 600-token block in the system prompt naming the project root, the directory shape two levels deep, the likely entry points, which commands are installed (and which are not), the three platform differences that change command syntax, and whether the semantic code index is built. The binary half extends the existing day-0 system-context probe rather than adding a second one: `probe_binaries` is now the single PATH probe, shared by `collect_system_info` and the map, and `DEV_TOOL_PROBES` widens the seven desktop-app markers to the developer toolchain. `index_codebase` gains its first automatic trigger. When the root satisfies `is_code_repository` — a VCS directory or a recognised manifest, non-recursive — and no index exists, the map starts one in a background thread and says so in the prompt. `GAIA_PROJECT_MAP_AUTO_INDEX=0` turns it off. Budget is 600 tokens, 1.8% of the 32K NPU window, enforced by `render_project_map` on every render and pinned by a test against a 200-directory repository. The map is cached per root and invalidated by a fingerprint over the top-level listing, manifest contents, VCS head and PATH.
… itself Review follow-ups on the task-start project map, each a bug a user would have hit: - The index line parsed the whole code-index metadata — every chunk's text — on every prompt composition, several times a turn. `CodeIndexSDK.is_indexed` answers the same question with two `exists()` calls. - A background index that died left the prompt saying "building now" for the rest of the session, so the model kept waiting for something that would never arrive. The trigger is now a four-state machine and inspects the tool's JSON, which is how `index_codebase` reports a refusal rather than raising. - The map told the model `run_shell_command` accepts `uv`, `npm` and `python`. It accepts none of them. Installed-and-allowlisted, installed-but-refused, and not-installed are now three separate lines — and the whole section is omitted for an agent that has no shell tool. - In dev mode the daemon launches the agent sidecar from the GAIA checkout, so the working directory resolved to GAIA's own source and it would have background-indexed itself. `is_agent_own_source` rejects that; an explicit `GAIA_PROJECT_ROOT` is exempt. - Listing `ProjectMapMixin` after the base agent silently disabled the index trigger while the prompt still rendered. `__init_subclass__` now raises. Also: the root resolves once per session so the map and the code index cannot describe two different trees, and the day-0 memory fact that duplicated `git`/`node`/`docker` (and carried an `except Exception: pass`) is gone — `DEV_TOOL_PROBES` feeds the map, which is all #3379 asked for.
Skill audit
✅ All audited skills cleared the tier they claim. Per-finding detail is withheld here on purpose. Read it in the Security > Code scanning tab, or download the |
Verdict: Request changesThis adds a ~600-token orientation block to the agent's system prompt — directory shape, entry points, installed commands, platform quirks — plus a first automatic trigger for code indexing. The static half genuinely works and is well tested; the dynamic half does not, and the docs promise it in four places. The map is built once and then frozen for the session. The agent's system prompt is only rebuilt when its tool or skill selection changes, so nothing re-renders the map when the project or the index state moves. The states this PR advertises — "building now in the background", "build FAILED" — never reach the model, and a directory created mid-session never shows up. That was observed on a real two-turn run, not inferred. Either refresh the prompt when the map's own content changes, or drop the changing-state language from the guide, the CHANGELOG and SPEC so the docs match what ships. A project with GAIA installed into a venv inside it gets no map at all. The "don't map GAIA's own source" guard rejects any directory that contains the running A vanished project directory takes the whole turn down. If the project root is deleted, renamed or unmounted mid-session, the task-start hook raises and the query fails instead of just losing the map. The prompt path already degrades gracefully here; the hook should too. Also worth noting before merge: this changes the system prompt, so CLAUDE.md requires an eval run. The PR is upfront that #3341 blocks it — please land the eval when that clears, since a new always-on prompt block is exactly the kind of change evals catch. Real-world evidenceAn evidence stage ran the gaia-testing skill on a no-inference The block captured from an actual Measured at 247 of the 600-token budget. The planted names are all present, so it is read off the real tree. Root resolution also held on a real launch — from the GAIA checkout with no override it declined to map itself: [project-map] /home/runner/work/gaia/gaia is GAIA's own source tree — no map.
Set GAIA_PROJECT_ROOT to the project you want mapped.The auto-index trigger fired and failed loudly when the embedding backend was down: INFO | [project-map] indexing /tmp/zephyr-proj in the background
ERROR | [project-map] background index of /tmp/zephyr-proj failed:
Request failed: HTTPConnectionPool(host='localhost', port=13305) ... Connection refusedBut the prompt never followed. Both turns of that run rendered the same line, after the failure: $ grep -o "Code index: [^\\]*" ~/.gaia/logs/gaia-agent.log
Code index: not built — call index_codebase to enable semantic code search
Code index: not built — call index_codebase to enable semantic code searchAnd a directory created between turn 1 and turn 2 of one process never appeared — both maps byte-identical. That is the first blocking finding above, confirmed against the code. The bundle also caught a Adjacent surfaces were spot-checked green: Deferred to the strix-halo lane and not covered by this verdict: the Agent UI screenshot, any real LLM turn, index completion (the "built — use search_code_index" line), 🔍 Technical details🟡 The rendered map is frozen after the first composition (
|
The agent used to open every task blind: it guessed directory names, guessed which programs were installed, and guessed which shell it was talking to, and a meaningful share of its wasted steps were those guesses coming back as "no such file" or "command not found" — each a full round trip to learn something one orientation pass establishes once. Inside a code repository it now starts every task with a 600-token project map in the system prompt: the root, the directory shape two levels deep, the likely entry points, which commands are installed and which of those the shell tool will actually accept, the three platform differences that change command syntax, and whether the semantic code index is built.
index_codebasealso gains its first automatic trigger — until now nothing but the model deciding to call it ever built an index.Closes #3379.
The four numbers and predicates the issue asked to be named
is_code_repository(path)— a directory fromVCS_DIRS(.git,.hg,.svn) or a file fromPROJECT_MANIFESTS(14 entries) at the root. Non-recursive, so a home directory full of repositories is not itself one. Every manifest in the list is parametrized in the tests.PlatformQuirks— path separator, path quoting for spaces, shell dialect. A test asserts the dataclass has those three fields and no others, so the list stays closed.probe_binaries()is now the single PATH probe in the codebase;collect_system_infoand the map both go through it. The extension over the seven commands it already detected (git,code,cursor,node,docker,brew,npm— chosen because they double as desktop-app markers) isDEV_TOOL_PROBES: 29 build, package, runtime and VCS binaries. The map also crosses the result withrun_shell_command's own allowlist, so it can say up front which commands the shell tool will refuse instead of letting the agent discover that one refusal at a time.Two behaviours worth a reviewer's attention
Auto-indexing costs something on first contact. When the root is a repository with no index, the map starts
index_codebasein a background thread. On a large repo that is minutes of local embedding, and the embedder can evict the resident chat model — the same trade the RAG warm-up already makes, so the cost is first-turn latency, not a wrong answer.GAIA_PROJECT_MAP_AUTO_INDEX=0turns it off. It fires at most once per session, and a failure is reported in the prompt asbuild FAILEDrather than leaving the model waiting on something that will never arrive.Root resolution refuses to point at GAIA itself. In dev mode the daemon launches the agent sidecar with its working directory set to the GAIA checkout, so a naive cwd rule would have had the flagship map — and background-index — its own source tree.
is_agent_own_sourcerejects that. An explicitGAIA_PROJECT_ROOTis exempt: pointing GAIA at GAIA is legitimate when you mean it.Eval not run: this changes the system prompt, which CLAUDE.md flags as eval-affecting. #3341 tracks
gaia eval agentfailing repo-wide because the API account behindANTHROPIC_API_KEYis out of credit.Test plan
python -m pytest tests/unit/test_project_map.py -q— 64 tests: the predicate against all 14 manifests, the budget on a 200×20-directory repo, cache invalidation on directory/manifest/PATH change, the four index-trigger states, and the MRO guard.python -m pytest tests/unit/ -q— no new failures against the pre-change baseline (616 failed / 9,621 passed before, 614 / 9,679 after; the deltas are the new tests). The email- and hub-agent modules fail identically before and after for an unrelated editable-install reason.python util/lint.py --all— black, isort, pylint, flake8, bandit and the agent-convention checks pass.GAIA_PROJECT_ROOT=$(pwd) python -c "from gaia.agents.base.project_map import *; from gaia.agents.base.turn_metrics import count_tokens; t=render_project_map(build_project_map(resolve_project_root())); print(t); print(count_tokens(t))"Expect ~470 tokens, and every name on the
run_shell_command accepts:line present inALLOWED_COMMANDS.resolve_project_root()returnsNoneand the prompt fragment is empty.