A from-scratch, provider-agnostic reasoning agent with a typed state substrate and verifier-guided search. Primary benchmark: GAIA.
Project description
banna
A from-scratch, provider-agnostic reasoning agent with a typed state substrate and a verifier-guided loop. Built to study where ReAct-style agents fail on the GAIA benchmark and to fix those failures structurally — not with prompt patches.
No LangChain, no LlamaIndex, no smolagents in the core. The reasoning loop is a typed transition function over (state, action, observation) → state'; ReAct, verifier-retry, planner-ReAct, BFS/DFS/best-first-over-plans, and best-of-N are each ~200 LOC Policy implementations over that same substrate.
What's interesting about this repo
- Forensic GAIA debugging. A full-validation run on
gpt-5-nanowas instrumented end-to-end, traces were dumped per task, and seven distinct structural failure modes were diagnosed and fixed — not by prompt-tweaking, but by changing the loop. See Failure modes & fixes below. - Multi-axis budget tracker that separates productive steps from repair steps. A model stuck in an
[empty_reply]loop no longer burns its productive-step budget; instead it trips a separatemax_repair_stepsaxis with a forced tool-choice escape. - Per-verifier actionable nudges. Each verifier (Arithmetic, Citation, Coverage, Format) attaches a
meta["nudge"]to its fail verdicts that names the missing thing (the recomputed value, the missing evidence_id, the unsupported number, the empty field). The retry policy groups these by verifier and emits one line per kind — short enough that the model actually reads them. - Budget-exhaustion synthesis. When the agent runs out of steps mid-task, instead of returning
null, a final forced-final_answercall gives it one last shot with a cheap fallback chain (last claim → last short text → none). - Provider-agnostic tool forcing. A single helper translates "force any tool" into OpenAI's
tool_choice: "required", Anthropic's{type: "any"}, and Gemini'sANYmode — used to break out of empty-reply loops.
Architecture
flowchart TB
Q[User question<br/>+ optional attachment] --> AG[Agent.run_policy]
subgraph SUB[banna_agent runtime]
AG --> ST[AgentState<br/>typed: Trace · Evidence · Claim · Budget]
ST -->|propose| POL{Policy}
POL --> REACT[ReActPolicy]
POL --> VR[VerifierRetryPolicy<br/>wraps inner]
POL --> PL[PlannerReActPolicy]
POL --> BFS[BFS / DFS / Best-First<br/>over plans]
POL --> BON[BestOfNPolicy<br/>K trajectories + selector]
REACT -->|Action| EX[Execute]
VR -->|FINAL_ANSWER| VERS{Verifiers}
VERS -- pass --> COMMIT[commit]
VERS -- fail --> NUDGE[per-verifier nudge<br/>→ THINK feedback]
NUDGE --> POL
EX --> TOOLS[ToolRegistry]
TOOLS --> T1[search]
TOOLS --> T2[read_url]
TOOLS --> T3[read_file<br/>+ pdf / xlsx tools]
TOOLS --> T4[python_sandbox]
TOOLS --> T5[calculator]
TOOLS --> T6[run_shell · grep · list_files]
EX -->|observation| ST
ST --> BUD[BudgetTracker<br/>steps · repair_steps · wall · tokens · cost]
BUD -. trip .-> SYN[synthesize_on_exhaustion<br/>forced final_answer]
SYN --> COMMIT
end
subgraph VERIFIERS[Verifiers]
VF[FormatVerifier<br/>shape / empty answer]
VA[ArithmeticVerifier<br/>safe-AST recompute]
VC[CitationVerifier<br/>numeric + Jaccard support]
VG[CoverageVerifier<br/>claim ↔ evidence]
end
VERS --- VF
VERS --- VA
VERS --- VC
VERS --- VG
COMMIT --> ANS[answer]
Install
# 1. From PyPI (once published)
pip install banna
# 2. From GitHub directly (no clone, no PyPI required)
pip install git+https://github.com/siavashmonfared/banna.git
# 3. Isolated install with pipx (recommended for CLI use)
pipx install git+https://github.com/siavashmonfared/banna.git
# 4. From a local clone (for development)
git clone https://github.com/siavashmonfared/banna.git
cd banna
pip install -e ".[dev]"
Any install path drops a banna (and banna-agent) executable on your $PATH.
Quickstart
# set at least one provider key
export OPENAI_API_KEY=sk-... # or ANTHROPIC_API_KEY=... / GEMINI_API_KEY=...
# open the interactive REPL
banna --policy verifier_retry --provider openai --model gpt-5-nano
# or run a single GAIA Level-1 question (no REPL)
python -m banna_agent.benchmarks.gaia.runner \
--policy verifier_retry --provider openai --model gpt-5-nano \
--level 1 --n 1
Example REPL session
$ banna --policy verifier_retry --provider openai --model gpt-5-nano
● banna · v0.1.0 provider=openai model=gpt-5-nano policy=verifier_retry
> How many studio albums did Mercedes Sosa release between 2000 and 2009?
thinking…
▸ search(query="Mercedes Sosa discography studio albums 2000-2009")
↳ 8 results · evidence_id ev_a3f
▸ read_url(url="https://en.wikipedia.org/wiki/Mercedes_Sosa")
↳ 12.4 kB · evidence_id ev_91c
thinking…
▸ final_answer(answer="3", evidence_ids=["ev_a3f", "ev_91c"])
verifiers: format ✓ citation ✓ coverage ✓ arithmetic skip
● banna
3
3 steps · 4.7s · 1840→210 tok · $0.0021
> /show trace
…step-by-step dump of action + observation + meta…
> /exit
The full GAIA validation runner (165 questions across L1/L2/L3) is in experiments/02_gaia_full/run.py.
Failure modes & fixes (the C1–C6 pass)
Diagnosed from a full GAIA validation run on gpt-5-nano. Each fix lands as a structural change to the loop, with unit tests pinning the new behavior.
| ID | Failure mode | Root cause | Fix |
|---|---|---|---|
| C1 | [empty_reply] loops eat the step budget |
Repair-style THINKs counted as productive steps | New Budget.repair_steps_used axis + max_repair_steps=6; meta["repair"]=True routes off the main counter |
| C2 | Model returns empty content + no tool call | No detection / no escape | After 2 consecutive empties with no evidence, force tool_choice to any tool (provider-agnostic) |
| C3 | pred_answer=null on budget exhaustion |
Loop exits with no commit | policy.synthesize_on_exhaustion(state): one threaded LLM call with forced final_answer + cheap fallback chain |
| C4 | L1 step cap too tight (8 steps) | Default budget profile | L1/L2/L3 caps bumped to 12/18/24 |
| C5 | Rich file tools never used on attachments | Hint steered model toward read_file even for PDF/XLSX |
Extension-routed _file_hint() + cheap _file_summary() (pypdf page count, openpyxl sheet names, CSV header) |
| C6 | Verifier retries low repair rate | Feedback was generic | Each verifier populates meta["nudge"] with a verifier-specific actionable instruction; retry feedback groups by verifier |
GAIA validation results
The fixes are landed and tested (562 tests passing in this public repo, 568 in the private superset). The post-fix re-run on GAIA validation is the next thing on the queue.
| Run | Provider · model | Policy | Set | Accuracy | Notes |
|---|---|---|---|---|---|
| Pre-C1–C6 | OpenAI · gpt-5-nano |
verifier_retry | GAIA val (165 Q) | 33.9 % (56 / 165) | $0.94, 7 structural bugs surfaced |
| Post-C1–C6 | OpenAI · gpt-5-nano |
verifier_retry | GAIA val (165 Q) | re-run pending | target ≥ 40 % |
| Post-C1–C6 | OpenAI · gpt-5-nano |
best_of_n (K=3) | GAIA val (165 Q) | re-run pending | stretch ≥ 45 % |
(Numbers populate once the re-run completes; CI is wired to gate on a held-out smoke subset.)
Repo layout
src/banna_agent/
├── core/ AgentState, Trace, Action, Budget, EventLog, run_policy
├── llm/ provider-agnostic LLMClient + adapters (anthropic, openai, gemini, ollama, bedrock)
├── tools/ search, read_url, read_file, pdf/xlsx tools, python_sandbox,
│ calculator, run_shell, grep, list_files, plan, memory, final_answer
├── policies/ react, planner_react, verifier_retry, bfs/dfs/best_first_over_plans, best_of_n
├── verifiers/ arithmetic, citation, coverage, format, command (+ base protocol)
├── benchmarks/ gaia/ (loader, runner, scorer, report)
├── memory/ in_memory_store, jsonl_store, skill_library, embeddings
└── cli/ Rich-based REPL: /policy /budget /show /skills /compact /save /load …
Tests live in tests/ and are organized to mirror src/. Run them with:
pytest -q
Current status: 562 passed, 0 failed on this public branch (no external substrate dependencies).
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file banna-0.1.0.tar.gz.
File metadata
- Download URL: banna-0.1.0.tar.gz
- Upload date:
- Size: 186.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
513245901a4c20f01a04d199fb15a071a0dd81aa084ff5f7030c9993b479476a
|
|
| MD5 |
fd76aa96a09f9c52d6db0c18df5af890
|
|
| BLAKE2b-256 |
c4ee144a9638f881779f247a9c978fef29df2eb6fcfe8089701cda2d6108330c
|
Provenance
The following attestation bundles were made for banna-0.1.0.tar.gz:
Publisher:
publish.yml on siavashmonfared/banna
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
banna-0.1.0.tar.gz -
Subject digest:
513245901a4c20f01a04d199fb15a071a0dd81aa084ff5f7030c9993b479476a - Sigstore transparency entry: 1573949035
- Sigstore integration time:
-
Permalink:
siavashmonfared/banna@66b3ead12e8275ed67f10afd476d46621c227e23 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/siavashmonfared
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@66b3ead12e8275ed67f10afd476d46621c227e23 -
Trigger Event:
push
-
Statement type:
File details
Details for the file banna-0.1.0-py3-none-any.whl.
File metadata
- Download URL: banna-0.1.0-py3-none-any.whl
- Upload date:
- Size: 237.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
576963961b6b698b7b015451b8259a51c368d99a83ac9ab1ef5657a858c53203
|
|
| MD5 |
6cae08d33fda2e3d620475a107235a15
|
|
| BLAKE2b-256 |
003ef54fbef9e5233f9007178ec4a565d2f4826d7d36c08660cffd91d95ef108
|
Provenance
The following attestation bundles were made for banna-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on siavashmonfared/banna
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
banna-0.1.0-py3-none-any.whl -
Subject digest:
576963961b6b698b7b015451b8259a51c368d99a83ac9ab1ef5657a858c53203 - Sigstore transparency entry: 1573949091
- Sigstore integration time:
-
Permalink:
siavashmonfared/banna@66b3ead12e8275ed67f10afd476d46621c227e23 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/siavashmonfared
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@66b3ead12e8275ed67f10afd476d46621c227e23 -
Trigger Event:
push
-
Statement type: