Metadata-Version: 2.5
Name: ramabana
Version: 0.1.3
Summary: rama's arrow. a harness that does not miss
Project-URL: Repository, https://github.com/vedicreader/ramabana
Project-URL: Documentation, https://vedicreader.github.io/ramabana/
Author-email: Karthik <karthik.rajgopal@hotmail.com>
License: Apache-2.0
License-File: LICENSE
Keywords: nbdev
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Requires-Python: >=3.12
Requires-Dist: exhash>=0.4.9
Requires-Dist: fastcore>=2.1.16
Requires-Dist: fastllm-claude-code>=0.0.7
Requires-Dist: fastspec>=0.2.1
Requires-Dist: fossick>=0.1.11
Requires-Dist: koshas>=0.0.31
Requires-Dist: litesearch>=0.1.1
Requires-Dist: pyyaml>=6.0
Requires-Dist: rgapi>=0.1.19
Requires-Dist: rishi>=0.1.12
Requires-Dist: vishalakshi>=0.1.7
Provides-Extra: all
Requires-Dist: dhrishti>=0.1.2; extra == 'all'
Requires-Dist: jupyter-client>=8.9.1; extra == 'all'
Requires-Dist: mcp>=1.2; extra == 'all'
Requires-Dist: pyghostty>=0.1.0; extra == 'all'
Requires-Dist: teleprint>=0.1.1; extra == 'all'
Provides-Extra: cli
Requires-Dist: pyghostty>=0.1.0; extra == 'cli'
Requires-Dist: teleprint>=0.1.1; extra == 'cli'
Provides-Extra: dev
Requires-Dist: dhrishti>=0.1.2; extra == 'dev'
Requires-Dist: jupyter-client>=8.9.1; extra == 'dev'
Requires-Dist: mcp>=1.2; extra == 'dev'
Requires-Dist: nbdev>=3.3.11; extra == 'dev'
Requires-Dist: notebook>=7.6.1; extra == 'dev'
Requires-Dist: pyghostty>=0.1.0; extra == 'dev'
Requires-Dist: pytest>=8.0.0; extra == 'dev'
Requires-Dist: rishi[all]>=0.1.12; extra == 'dev'
Requires-Dist: teleprint>=0.1.1; extra == 'dev'
Provides-Extra: mcp
Requires-Dist: mcp>=1.2; extra == 'mcp'
Provides-Extra: pyrepl
Requires-Dist: dhrishti>=0.1.2; extra == 'pyrepl'
Requires-Dist: jupyter-client>=8.9.1; extra == 'pyrepl'
Requires-Dist: pyghostty>=0.1.0; extra == 'pyrepl'
Requires-Dist: teleprint>=0.1.1; extra == 'pyrepl'
Description-Content-Type: text/markdown

# ramabana


<!-- WARNING: THIS FILE WAS AUTOGENERATED! DO NOT EDIT! -->

ramabana is the **brain** of a coding agent: policy, tools, memory, and routing. It has no editor
and no frontend. The application it runs inside speaks one protocol, [`Host`](https://vedicreader.github.io/ramabana/tools.html#host); models arrive through
[rishi](https://github.com/vedicreader/rishi).

It grew up inside [leela](https://github.com/vedicreader/leela) and never imported the IDE, so it
now stands alone with leela as its first consumer. It ships two frontends of its own – a terminal
app and an MCP server – on that same [`Host`](https://vedicreader.github.io/ramabana/tools.html#host).

## What you get

| Capability | Built on | What it does |
|----|----|----|
| Any model runtime | [rishi](https://github.com/vedicreader/rishi) | LiteRT, MLX, llama.cpp, Cursor and hosted APIs behind one `Chat` |
| Code index | [kosha](https://github.com/vedicreader/kosha) | FTS + vectors + call graph over the open folders *and* installed packages |
| Search fusion | [litesearch](https://github.com/vedicreader/litesearch) | one ranking from kosha, ripgrep and vault prose (RRF) |
| Web research | [fossick](https://github.com/vedicreader/fossick) | search, escalating fetch, cited digests, shopping carts |
| Durable memory | [vishalakshi](https://github.com/vedicreader/vishalakshi) | a vault of what was read, federated search, watches |
| Editing | [exhash](https://github.com/vedicreader/exhash) | hash-verified line addressing, so a stale read fails instead of damaging a file |

Two decisions shape everything else. **Routing** keeps the cheap jobs – labels, summaries, inline
completion, the compaction checkpoint – on a small local model while the turn runs wherever you
chose. **Capability probing** means a tool the host cannot perform is never offered to the model,
so a partly built host gives a smaller agent rather than a broken one.

## Modules

One notebook per module – the page you read is the module you import.

| notebook | module | owns |
|----|----|----|
| `00_core` | `ramabana.core` | errors, environment, and which model runs which job |
| `01_runtime` | `ramabana.runtime` | the rishi adapter, usage, native diagnostics, compaction |
| `02_tools` | `ramabana.tools` | [`Host`](https://vedicreader.github.io/ramabana/tools.html#host), [`LocalHost`](https://vedicreader.github.io/ramabana/tools.html#localhost), the tools, skills, extensions, sub-agents |
| `03_agent` | `ramabana.agent` | approvals, the activity feed, [`Agent`](https://vedicreader.github.io/ramabana/agent.html#agent), inline completion |
| `04_testing` | `ramabana.testing` | a full-capability host and the backend doubles |
| `05_cli` | `ramabana.cli` | the terminal app, on [teleprint](https://github.com/answerdotai/teleprint) |
| `06_mcp` | `ramabana.mcp` | the same tools, served over MCP |
| `07_vault` | `ramabana.vault` | durable memory, federated search and watches |
| `08_shop` | `ramabana.shop` | a trolley an agent can fill, on fossick |
| `09_coding_patterns` | `ramabana.coding_patterns` | the coding standards, published as a skill |
| `10_spec` | `ramabana.spec` | OpenAPI, Azure and Google Discovery specs, called as tools |
| `11_pyrepl` | `ramabana.pyrepl` | the kernel and overlay behind `--python`, on [dhrishti](https://github.com/vedicreader/dhrishti) |

## Install

``` sh
pip install ramabana                 # the harness
pip install 'ramabana[cli]'          # ...and the terminal app
pip install 'ramabana[pyrepl]'       # ...and the Python prompt inside it
pip install 'ramabana[mcp]'          # ...and the MCP server
pip install 'ramabana[all]'
```

A model is not bundled. `rishi` fetches one on first use, and which one is a routing decision –
see [core](00_core.ipynb). LiteRT models run on the GPU and fall back to the CPU with a warning
where there is no delegate; `RAMABANA_LITERT_BACKEND=cpu` pins them to the CPU.

## Use

An agent needs a host, and [`LocalHost`](https://vedicreader.github.io/ramabana/tools.html#localhost) is one over real folders. Nothing was downloaded here, so
the agent is not ready – and it says so instead of raising.

``` python
from ramabana import Agent
from ramabana.tools import LocalHost, tools_for

host = LocalHost(['..'], web=True)
agent = Agent(host, extensions=False)
len(agent.tools), agent.ready, agent.note
```

    (23, False, 'not started')

[`LocalHost`](https://vedicreader.github.io/ramabana/tools.html#localhost) starts `Kosha.sync` automatically, in a daemon thread, over every open root. Indexing
overlaps model startup instead of delaying the first prompt, and search falls back to a literal
scan until the semantic + keyword index is ready.

``` python
host.wait_index(120), host.search_note
```

    (True, 'Kosha semantic + keyword index over 1 folder(s) and environment')

``` python
[(h.path, h.line, h.symbol) for h in host.search('drop the thinking from a streamed reply')[:3]]
```

    [('/Users/71293/code/personal/orgs/ramabana/ramabana/runtime.py',
      933,
      'ramabana.runtime.RishiBackend._stream'),
     ('/Users/71293/code/personal/orgs/ramabana/ramabana/runtime.py',
      717,
      'ramabana.runtime.ThinkFilter'),
     ('/Users/71293/code/personal/orgs/ramabana/ramabana/cli.py',
      273,
      'ramabana.cli.Ui.stream')]

Anything the host cannot do is not offered to the model at all:

``` python
from ramabana.tools import NullHost
len(tools_for(NullHost())), len(tools_for(host))
```

    (10, 19)

The sandbox has two halves. Confining *writes* stops an agent damaging something nobody opened;
confining *reads* stops it answering from a sibling checkout, a library outside the venv, or a log
the project does not own – which is a cost as often as a protection. `read_outside=True` separates
them: a read may name any path on the machine, a write may not, and credential paths are refused
either way. Enumeration stays confined regardless, so `walk`, `grep` and `list_files` never leave
the open folders and a read outside is always a path the model already knew.

``` python
open_host = LocalHost(['..'], web=False, index=False, read_outside=True)
open_host.roots_note
```

One turn is `ask`. Here it runs against a scripted backend so the page is reproducible;
[testing](04_testing.ipynb) is where that comes from.

``` python
from ramabana.testing import fake_agent

scripted, backend = fake_agent(replies=['`threshold` is in `ramabana/runtime.py`.'])
scripted.ask('where is the compaction threshold?')
```

    '`threshold` is in `ramabana/runtime.py`.'

The turn is inspectable afterwards – what it called, what it changed, what it cost:

``` python
scripted.calls, scripted.changes(), repr(scripted.use)
```

    ([('search_code', {'query': 'where is the compaction threshold?'})],
     {},
     '15 tok · in 10 · out 5 · model')

## In a terminal

``` sh
ramabana --root . --model gpt-mini      # a transcript of blocks, a status bar, one line to type in
ramabana --python --root .              # the same program, with Python as a mode inside it
ramabana-mcp --root .                   # the same tools, served to another agent
ramabana --approve auto --prompt "$TASK"  # no terminal at all: the answer, on stdout
```

In the terminal app ([cli](05_cli.ipynb)): tool calls are foldable blocks rather than lines, writes
stop for approval, a request whose reading matters (“refactor the runtime”) offers a row of options
before it becomes a turn, and every slash command is the agent’s own. Drop or paste a path to attach
a picture or a sound file, `@path` names one inline, and `/copy` (or `y` in the transcript view)
yields the model’s own Markdown rather than the terminal’s wrapping of it.

At the Python prompt ([pyrepl](11_pyrepl.ipynb)): `/python` takes the line and `/agent` hands it
back. The namespace belongs to whoever is typing – Ramabana reads it and builds in a layer of its
own, and cannot rebind a name it did not create. Tab completes from the live kernel with each
candidate’s type and shape beside it, and `/promote NAME` adopts one of the agent’s variables into
your namespace.

Over MCP ([mcp](06_mcp.ipynb)): read-only by default, writes behind `--write`, and `--model` adds an
`ask` tool that runs a whole Ramabana turn and returns just its answer.

`--read_outside` is the switch described above, and `--vault` keeps what a session reads in a
[vishalakshi](https://github.com/vedicreader/vishalakshi) vault, which adds the five `memory_*`
tools so the next session starts knowing what this one learned. `--spec` adds `api_load`, `api_ops`
and `api_call` ([spec](10_spec.ipynb)).

## One hard problem, end to end

Everything above is hermetic. Everything below is not: it runs a real agent over this repository,
against the real internet. The cells are `eval: false` so neither CI nor the docs build tries them
– **the outputs shown are real**, captured on an Apple silicon laptop.

The problem needs both halves of the harness: one fact is in this repo and only a code index finds
it, the other is a price on a supermarket site that turns scrapers away. The turn drives a
twenty-three tool loop on a hosted model while every cheap job runs on a 9B on the laptop.

``` python
from ramabana.agent import Agent
from ramabana.tools import LocalHost

host = LocalHost(['..'], web=True)          # this repository, and the network
host.wait_index(600)                        # let Kosha finish, so search is semantic

agent = Agent(host, model='gpt-mini', extensions=False)
for job in ('inline', 'completion', 'classify', 'summary', 'subagent'):
    agent.routing.set('ornith-9b', job)
agent.start() is not None, agent.note, len(agent.tools)
```

    (True, 'gpt-mini · cloud · 1050k ctx · 23 tools', 23)

Two engines for six jobs, and only one of them is remote:

``` python
print(agent.routing.summary())
```

    turn        gpt-mini · cloud · 1050k ctx
    inline      ornith-9b · local · 32k ctx
    completion  ornith-9b · local · 32k ctx
    classify    ornith-9b · local · 32k ctx
    summary     ornith-9b · local · 32k ctx
    subagent    ornith-9b · local · 32k ctx

``` python
sorted(agent.routing.backends())
```

    [('mlx', 'mlx-community/Ornith-1.0-9B-8bit'),
     ('remote', 'openai/gpt-5.6-luna')]

Now the question:

``` python
TASK = ("Two facts, then one line of arithmetic.\n"
        "1. In this repository, find the constant the compactor holds back for the model's reply, "
        "and say what it is called and what it is set to.\n"
        "2. Find what Arnott's Tim Tam Original 200g costs at Coles right now, in AUD.\n"
        "Finish with one line: the constant, the price, and how many packs those tokens would "
        "buy at $0.001 per token.")

answer = agent.ask(TASK)
print(answer)
```

    1. The compactor’s reply headroom constant is **`RESERVE`**, set to **16,384 tokens**.
    2. Arnott’s Tim Tam Original 200g costs **AUD $6.00** at Coles. ([Coles](https://www.coles.com.au/product/arnott's-tim-tam-chocolate-biscuits-original-200g-329607))

    **`RESERVE` (16,384 tokens) × $0.001/token = $16.384 ÷ $6.00 = 2.7307 packs** (2 whole packs).

Both halves are right, and both were *found* rather than recalled: `RESERVE = 16_384` really is the
compactor’s output reserve in `ramabana/runtime.py`, and \$6.00 really was the shelf price.

``` python
for name, args in agent.calls: print(name, {k: str(v)[:64] for k, v in args.items()})
```

    search_code {'query': 'Two facts, then one line of arithmetic.\n1. In this repositor'}
    search_code {'query': 'compactor holds back model reply constant tokens reply budget'}
    view_file {'path': '/Users/71293/code/personal/orgs/ramabana/ramabana/runtime.py', 'start': '230', 'end': '285'}
    web_search {'query': "site:coles.com.au Arnott's Tim Tam Original 200g price"}
    read_url {'url': "https://www.coles.com.au/product/arnott's-tim-tam-chocolate-bis", 'remember': 'False'}

Five calls: search the index, search again with better words, read the one file that matched, search
the web, read the one page that mattered. Nothing was written – and `changes()` is how a frontend
knows that without diffing the disk.

``` python
repr(agent.use), agent.changes(), agent.problems
```

    ('43,830 tok · in 43,179 · out 651 · cached 90% · gpt-5.6-luna', {}, [])

`read_url` is doing more than it looks. Coles answers a plain fetch with `200 OK` and an empty
shell, so escalating on the status code never fires; and the price is not in the page’s prose at all,
because readability extraction keeps the ingredient list and throws the price away. So the host
judges the extracted *text*, escalates to a real browser when there is too little of it to be a page,
and hands the model the page’s `schema.org` JSON-LD alongside the prose. That block is a standard,
not a selector for one shop:

``` python
import json
page = host.read_url("https://www.coles.com.au/product/arnott's-tim-tam-chocolate-biscuits-original-200g-329607")
ld = json.loads(page.text.partition('</structured-data>')[0].removeprefix('<structured-data>\n'))
ld[0]['name'], ld[0]['offers'][0]['price'], ld[0]['offers'][0]['priceCurrency']
```

    ("Arnott's Tim Tam Chocolate Biscuits Original | 200g", 6, 'AUD')

The cheap jobs ran on the 9B on the laptop. A label costs thirty-two tokens of output, so a
reasoning model has to be told *not* to think: asked to deliberate inside that budget it spends all
of it deliberating and there is no answer left to strip the thinking off of. See
[runtime](01_runtime.ipynb#answers-without-the-thinking).

``` python
agent.classify('the price came back from coles', ['success', 'failure'])
```

    'success'

``` python
agent.summarise(answer)
```

    "The compactor's reply headroom of 16,384 tokens (valued at $16.38) is sufficient to purchase 2 packs of Arnott's Tim Tam Original 200g biscuits at $6.00 each."

The same task without the terminal, for a pipe – the answer on stdout and nothing else:

``` sh
$ ramabana --model gpt-mini --approve auto --prompt "$TASK"
1. **Constant:** `RESERVE`, set to **16,384 tokens** -- headroom for the model's reply and tool results.
2. **Coles price:** Arnott's Tim Tam Original 200g is **A$6.00**.

**`RESERVE` (16,384 tokens) × A$0.001 = A$16.384 ÷ A$6.00 = 2.7307 packs (2 whole packs).**
```

Running the turn on the local model is one flag, and the reply is the reply – a template-primed
reasoning model’s deliberation never reaches the transcript:

``` sh
$ ramabana --model ornith-9b --prompt 'Reply with exactly the word: pong'
pong
```

## Develop

The notebooks in `nbs/` are the source. Never edit the generated modules.

``` sh
uv sync --all-extras --group dev
uv run nbdev-export           # notebooks -> ramabana/*.py
uv run nbdev-test             # execute every notebook
uv run pytest                 # the plain-python suite, one file per feature block
uv run nbdev-clean            # before committing
```

`nbdev-test` skips nothing by default; the real-model cells above are `eval: false` and are never
executed by it.
