Spaces:
Sleeping
A newer version of the Gradio SDK is available: 6.20.0
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Commands
pip install -e ".[test]" # editable install + test extras
lilith # interactive TUI (entry: lilith_agent.tui:main)
python -m lilith_agent.tui # equivalent
pytest # full suite (pyproject pins src/ on pythonpath)
pytest tests/test_graph.py -k router # single test by name
python scripts/dev_run_gaia.py --limit 3 --level 1
python scripts/dev_run_gaia.py --task-id <uuid> # rerun one question
python scripts/dev_run_gaia.py --split test --level 1 --limit -1 --cavemen --caveman-mode ultra
python scripts/build_leaderboard_submission.py --split test --out submission.jsonl
docker build -t lilith-pysandbox:latest sandbox/ # required before LILITH_SANDBOX=docker
Per-question state lives in .checkpoints/<task_id>.json β delete one to force a rerun. Session logs/traces land in .lilith/session-<ts>.{log,jsonl}. Both directories are gitignored.
Two app.py files exist: top-level app.py is the Gradio Space entry; src/lilith_agent/app.py is the ReAct graph. Don't confuse them.
Architecture
ReAct graph (src/lilith_agent/app.py)
build_react_agent(cfg) returns a compiled LangGraph StateGraph(AgentState) with three nodes β model, tools, fail_safe β and a MemorySaver checkpointer. The graph is the single source of truth for control flow; do not bypass it.
State machine:
modelβtoolswhen lastAIMessagehastool_callsAND iterations <recursion_limit-2AND tool calls since lastHumanMessage<budget_hard_cap(default 25).modelβfail_safewhen either ceiling is hit.fail_safe_nodeinvokes the model once with an emergency override prompt and ends.modelβENDwhen no tool calls (final answer).toolsβmodelwith appendedToolMessages (one per requested call).
The tools node (_build_tool_node) layers three independent guards before invoking a tool β keep them in this order if editing:
- Exact dedup:
(tool_name, sorted-json args)matches any priorAIMessagetool call β synthetic errorToolMessage, no invocation. - Semantic dedup (
web_searchonly): Jaccard similarity β₯cfg.semantic_dedup_threshold(default 0.5, tokens normalized + stopwords stripped) against any priorweb_searchquery in the current turn β synthetic error. - Per-tool error cooldown: 3 contiguous error
ToolMessages for one tool name β "stalled" error pushing the model to pivot strategy.
Tool exceptions are caught and surfaced as status="error" ToolMessages (never raised). This is what lets the model self-correct β preserve it.
The model node always runs three pre-invocation transforms in order:
apply_caveman(base_prompt, cfg.caveman, cfg.caveman_mode)wraps the system prompt with caveman framing when enabled._compact_old_tool_messagesβ keep last 4ToolMessages verbatim, compact older ones >300 chars. Ifcfg.compact_summarizeis on, the cheap model summarizes (target β€600 chars, prefixed[COMPACTED SUMMARY]so subsequent passes skip re-summarizing). Otherwise head-truncate with a[COMPACTED: N chars dropped]marker.- Goal re-injection at β₯5 calls and
[BUDGET WARNING]atcfg.budget_warn_atcalls β both as ephemeralSystemMessages prepended each turn (not stored in state).
Untrusted-input boundary: the user's first HumanMessage is wrapped in <gaia_question>...</gaia_question> by upstream callers. The model node strips this delimiter when extracting the goal. The system prompt instructs the model to treat anything inside as data, not instructions. Do not weaken this when refactoring prompts.
Config (src/lilith_agent/config.py)
Config.from_env() reads GAIA_* env vars (note: GAIA_ prefix, not LILITH_). Three model tiers: cheap / strong / extra_strong, each with independent provider+model. The agent's main model defaults to extra_strong, can select cheap or strong via GAIA_AGENT_MODEL_TIER, and can be directly overridden with GAIA_AGENT_PROVIDER / GAIA_AGENT_MODEL; cheap powers the tool-result summarizer. extra_strong_* defaults to strong_* if unset. DeepSeek is available as provider deepseek using DEEPSEEK_API_KEY or GAIA_DEEPSEEK_API_KEY. The vision_* pair is separate, used by inspect_visual_content. Behavior flags: caveman, caveman_mode, recursion_limit (50), budget_hard_cap (25), budget_warn_at (15), semantic_dedup_threshold (0.5), compact_summarize, llm_formatter_enabled.
Tools (src/lilith_agent/tools/)
build_tools(cfg) in tools/__init__.py returns the registered list. Tools are LangChain @tool-decorated closures; the closure injects cfg so individual tool modules stay pure functions.
run_python (tools/python_exec.py) runs in an isolated sandbox with no host filesystem access. Selection via LILITH_SANDBOX: auto (default β docker if available, else process), process, docker. Cross-boundary I/O must route through read_file (in) and write_file (out, restricted to .lilith/scratch). Files written from inside run_python vanish when the call returns. The Docker backend requires lilith-pysandbox:latest to be built (sandbox/Dockerfile); see sandbox/README.md for the isolation matrix and known gaps (e.g., ctypes-level metadata-IP bypass).
inspect_visual_content has a fallback chain: configured provider+model β same-provider stable fallback β cross-provider last-resort (gemini-3-flash-preview on Google). All-fail trips a session-level circuit breaker β future calls return a clean error rather than retry. Don't add retry loops above this; the breaker is intentional.
Observability (src/lilith_agent/observability.py)
Two logger trees configured by setup_observability():
lilith_agent.appβ routing/compactionlilith_agent.nodes.{model,tools,fail_safe}β per-node traces (chosen so output mirrors the GAIA reference agent's bracketed-tag style)
JsonlTraceCallback writes one JSON event per line to .lilith/session-*.jsonl flushed per write β replay-able. Arize AX auto-enables when ARIZE_SPACE_ID + ARIZE_API_KEY are set; LangSmith via LANGCHAIN_TRACING_V2=true + LANGCHAIN_API_KEY + LANGCHAIN_PROJECT. Token-usage metadata in response_metadata is preserved (only safety_ratings, logprobs, Gemini thought signatures are stripped β keep this allowlist when touching _strip_response_metadata_noise, since Arize cost reporting depends on input_tokens/output_tokens).
Entry points
tui.pyβ interactive REPL with slash commands (/clear,/caveman [on|off|lite|full|ultra]).runner.py::run_agent_on_questionsβ batch over GAIA, writes.checkpoints/<task_id>.jsonper question; reruns skip existing checkpoints unless--force.app.py(top-level) β Gradio Space; callsbuild_react_agent+run_agent_on_questions+ScoringApiClient(agents-course-unit4-scoring.hf.space).gaia_dataset.pyβ HF dataset client (GaiaDatasetClient), needsGAIA_HUGGINGFACE_API_KEY.
Repo-specific notes
- Python β₯3.11 required (
pyproject.toml). requirements.txtis the pinned snapshot for the HF Space (includesgradio,datasets,requestsnot inpyprojectdeps);pyproject.tomlis the editable-install spec. Keep both in sync when adding deps that the runtime touches.ffmpegon PATH is preferred foryoutube_frame_at; falls back toimageio-ffmpeg..last_failures.txtis written by the GAIA runner β useful for--forcereruns of just the failed task IDs.- Never weaken the
<gaia_question>untrusted-input wrapper, the per-tool cooldown, or the run_python FS boundary without explicit instruction β these are intentional hardening, not artifacts.
Agent Behavioral Guidelines (Karpathy Style)
- Think Before Coding: Don't assume. Surface tradeoffs. If multiple interpretations exist, present them. If unclear, stop and ask.
- Simplicity First: Write the minimum code that solves the problem. No speculative features, no "just-in-case" abstractions or "flexibility" that wasn't requested.
- Surgical Changes: Touch only what you must. Don't "improve" adjacent code, comments, or formatting. Don't refactor things that aren't broken. Match existing style. Every changed line should trace directly to the requested task. Clean up only your own orphans (imports/variables).
- Goal-Driven Execution: Define success criteria. Loop until verified. Transform tasks into verifiable goals (e.g., "Write a test that reproduces the bug, then make it pass").
- Engineering Realism (March of Nines): Prioritize deployment reliability and tail-end behavior over happy-path demos. Acknowledge that the leap from 90% to 99.9% is non-linear.