Metadata-Version: 2.5
Name: ramabana
Version: 0.1.1
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
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: fossick>=0.1.0
Requires-Dist: koshas>=0.0.31
Requires-Dist: rishi>=0.1.1
Requires-Dist: vishalakshi>=0.0.1
Provides-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: mcp>=1.2; extra == 'dev'
Requires-Dist: nbdev>=3.3; extra == 'dev'
Requires-Dist: notebook>=7.6.0; extra == 'dev'
Requires-Dist: pyghostty>=0.1.0; extra == 'dev'
Requires-Dist: pytest>=8.0.0; extra == 'dev'
Requires-Dist: rishi[all]>=0.1.1; extra == 'dev'
Requires-Dist: teleprint>=0.1.1; extra == 'dev'
Requires-Dist: twine>=7.0.0; extra == 'dev'
Provides-Extra: mcp
Requires-Dist: mcp>=1.2; extra == 'mcp'
Description-Content-Type: text/markdown

# ramabana


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

ramabana is the *brain* of a coding agent, and nothing else. It has no editor, no
frontend and no opinion about where it is running: everything it needs from the
application around it arrives through one protocol, [`Host`](https://vedicreader.github.io/ramabana/tools.html#host), and everything it needs
from a model arrives through [rishi](https://github.com/vedicreader/rishi).

It grew up inside [leela](https://github.com/vedicreader/leela), where it was already
written to this seam – nothing in it ever imported the IDE – and now stands on its own,
with leela as its first consumer. It also ships its own two frontends, because a brain
nobody can talk to is hard to judge: a terminal app and an MCP server, both built on the
same [`Host`](https://vedicreader.github.io/ramabana/tools.html#host).

## What is in it

The nbdev source is nine notebooks, and each one is exactly one module – the page you
read and the module you import are the same thing.

| 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, on [vishalakshi](https://github.com/vedicreader/vishalakshi) |
| `08_shop` | `ramabana.shop` | a trolley an agent can fill, on [fossick](https://github.com/vedicreader/fossick) |

Every model runtime – LiteRT, MLX, llama.cpp and hosted providers – arrives through
`rishi.Chat`. Ramabana owns agent policy, not provider-specific model loops.

## Install

``` sh
pip install ramabana                 # the harness
pip install 'ramabana[cli]'          # ...and the terminal app
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). To run LiteRT models on its GPU backend, set the
process-wide environment variable `RAMABANA_LITERT_BACKEND=gpu` before starting Ramabana.

## Use

An agent needs a host, and [`LocalHost`](https://vedicreader.github.io/ramabana/tools.html#localhost) is one over real folders:

``` 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; search uses a literal
fallback 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')]

Nothing was downloaded, so the agent is not ready – and it says so instead of raising. A
model is a multi-gigabyte download on one side and an API key on the other, and an editor
that will not open without either is a worse editor.

Anything the host cannot do is not offered to the model at all, so a partially built host
gives a smaller agent rather than a broken one:

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

    (10, 19)

With a model in place, one turn is `ask`. Here it runs against a scripted backend, so this
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')

## One hard problem, end to end

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

The problem is deliberately one that no single tool answers. Half of it is in this repo and
only a code index can find it; the other half is a price on a supermarket website that
blocks scrapers; and the last line needs both halves at once.

Routing is the point. The turn drives a twenty-three tool loop and goes to a hosted model;
every cheap job – the labels, the summaries, the inline completions, the compaction
checkpoint that keeps a long conversation inside its window – stays on a local MLX model and
never leaves the machine.

``` 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 is the shelf
price – fetched, not remembered from training.

The turn is auditable afterwards, which is the part that matters more than the answer:

``` 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 it 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 – 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')

And the cheap jobs ran on the 9B on the laptop, not on the hosted model. 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."

## In a terminal

``` sh
ramabana --root . --model gpt-mini
```

A transcript of blocks, a status bar, and one line to type in. Tool calls are foldable blocks
rather than lines, writes stop for approval, and every slash command is the agent’s own – see
[cli](05_cli.ipynb).

The same task, without the terminal, for a pipe:

``` 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 instead 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
```

## As an MCP server

``` sh
ramabana-mcp --root .
```

The same tools, served to another agent: read-only by default, writes behind `--write`, and
`--model` adds an `ask` tool that runs a whole Ramabana turn and returns just its answer –
see [mcp](06_mcp.ipynb).

## Develop

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

``` sh
uv sync --extra dev
uv run nbdev-export           # notebooks -> ramabana/*.py
uv run nbdev-test             # execute every notebook
uv run pytest                 # the plain-python suite
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.
