getattr
def __getattr__(
name
):The main a console script in SCRIPTS calls itself, so call_parse reads the command line; or one naming the missing extra.
--doctorramabana --doctor says what it found.
This module holds what has to happen before a session starts. The terminal session runs best inside tmux, a terminal multiplexer that divides one window into panes. tmux gives Ramabana a split for the now pane, the live view of the turn described in pane, and with its extended keys setting on it delivers shift+enter to the program as a key of its own, which the session uses to queue a message for the next turn. So the first run starts Ramabana inside a tmux server of its own, offers to install tmux when it is missing, and ramabana --doctor reports what it found. The module also decides whether the code index needs rebuilding.
Every console script that the package installs starts in this module, so it imports only fastcore and the standard library, and never ramabana.core. That keeps a script quick to start and lets it check for its dependencies before it imports them.
installed(mod) says whether a module would import, without importing it. need(extra) checks the modules an extra provides, as listed in EXTRAS, and returns the install command for the ones missing, or '' when all are there. SCRIPTS names, for each console script other than ramabana, the module whose main it runs and the extra it needs. Looking up one of those names on this module returns that main, or, when its extra is missing, a stand-in that prints the install command and exits with status 2. ramabana.core re-exports installed, need, EXTRAS and SCRIPTS.
The main a console script in SCRIPTS calls itself, so call_parse reads the command line; or one naming the missing extra.
'' when mods, by default all of extra’s, are installed; else what to install.
The env prefixes core reads: its own once loaded, where an app may have changed them, else ENV_DEFAULT.
assert installed('fastcore') and not installed('no_such_module')
test_eq(need('search', 'fastcore'), '')
test_eq(need('search', 'no_such_module'), "no_such_module not installed: pip install 'ramabana[search]'")
test_eq(env_prefixes(), (sys.modules['ramabana.core'].ENV_PREFIX, sys.modules['ramabana.core'].ENV_FALLBACK) if 'ramabana.core' in sys.modules else ENV_DEFAULT)Each session gets a tmux server of its own, started as tmux -L ramabana-<id> with the config file <cfg>/tmux.conf. A new server reads the current config and inherits the current environment, so a second session never picks up the first one’s API keys or virtual environment. That server never reads ~/.tmux.conf, and your own tmux setup is left as it was.
Extended keys are a tmux setting that passes key combinations such as shift+enter through to the program in the pane, instead of folding them into plain enter. tmux_conf(version) returns the config for a given tmux version, adding only the lines that version understands: extended-keys arrived in tmux 3.2 and extended-keys-format csi-u in 3.5, and an older tmux prints an error for each line it does not know. tmux_version reads the version from the output of tmux -V. The config claims true colour (RGB) only when $COLORTERM says the terminal draws it. The cell below prints the config for tmux 3.5.
The (major, minor) in tmux -V output such as tmux 3.5a, or None.
test_eq([tmux_version(s) for s in ('tmux 3.5a', 'tmux next-3.6', 'tmux master')], [(3, 5), (3, 6), None])
assert 'extended-keys-format csi-u' in tmux_conf((3, 5)) and 'csi-u' not in tmux_conf((3, 4))
assert 'extended-keys on' in tmux_conf((3, 2)) and 'extended-keys' not in tmux_conf((3, 1))
print(tmux_conf((3, 5)))Setup collects everything about the tmux that Ramabana runs in: the config, the relaunch into it, the install offer and --doctor.
Setup.launch runs before an interactive session starts. Outside tmux it replaces the current process with tmux new-session on a new server, running the same command line in the same folder. Quitting Ramabana then ends that tmux session and returns you to your shell. launch does nothing for a one-shot prompt, for --json, when stdin or stdout is not a terminal, and when --tmux off or $RAMABANA_TMUX=off says to stay out of tmux. It also needs tmux 3.0 or later. The relaunched process has $RAMABANA_WRAPPED set, so it never relaunches a second time. Inside tmux, --pane auto then opens the now pane.
The server is set to destroy-unattached, which ends the session when you detach or close the window, so Ramabana never keeps running where nobody can see it. The history is saved, and --resume latest reopens it. The command runs under sh with a short wrapper, HOLD, that waits for enter after a non-zero exit, so an error stays on screen instead of vanishing with the tmux session.
Inside a tmux server of your own, launch turns extended-keys on for that server when it is off. The change lasts until the server exits, and only programs that ask for extended keys receive them.
Every effect on the outside world goes through an argument of Setup: which, system, run, execvpe, ask and isatty, plus environ, argv and cwd. The examples pass fakes for all of them, so they run without tmux and change nothing.
def Setup(
cfg:str='~/.config/ramabana', # holds `tmux.conf` and `setup.json`
environ:NoneType=None, # default `os.environ`
argv:NoneType=None, # the command line to rerun; default `sys.argv`
cwd:NoneType=None, # where the session starts; default the current folder
which:function=which, system:function=system, run:function=run, execvpe:function=execvpe, ask:function=_no_stdin,
isatty:NoneType=None, # whether stdin and stdout are terminals
):The tmux ramabana runs in: its config, the relaunch into it, the install offer, and --doctor.
The example builds a fake machine, Box, that has tmux 3.5 on PATH and records each command run, each question asked and the execvpe it would make. launch reruns the command line on a new tmux server, starting in /work, with $RAMABANA_WRAPPED set and the original options at the end:
from types import SimpleNamespace
import tempfile
class Box:
"Programs on PATH, the commands run, the prompts shown and the exec made."
def __init__(self, have=('tmux',), version='tmux 3.5a', system='Darwin', answer='', ext='on'):
self.have, self.version, self.system, self.answer, self.ext = set(have), version, system, answer, ext
self.calls, self.asked, self.execd = [], [], None
def which(self, name): return f'/bin/{name}' if name in self.have else None
def run(self, cmd, **kw):
self.calls.append(list(cmd))
if 'install' in cmd: self.have.add('tmux')
return SimpleNamespace(returncode=0, stdout={'-V': self.version, 'show': self.ext}.get(cmd[1], ''))
def ask(self, q): return self.asked.append(q) or self.answer
def execvpe(self, *a): self.execd = a
def mk(box, **kw):
return Setup(tempfile.mkdtemp(), environ=kw.pop('environ', {'PATH': '/bin'}), argv=['-c', '--pane', 'on'], cwd='/work',
which=box.which, system=lambda: box.system, run=box.run, execvpe=box.execvpe, ask=box.ask, isatty=lambda: True)
box = Box()
mk(box).launch()
exe, argv, env = box.execd
test_eq((exe, env['RAMABANA_WRAPPED'], argv[argv.index('-c'):][:2], argv[-2:]), ('/bin/tmux', '1', ['-c', '/work'], ['--pane', 'on']))
argv[:12]['tmux',
'-L',
'ramabana-3cf00004',
'-f',
'/tmp/tmpol68wm_k/tmux.conf',
'new-session',
'-s',
'ramabana',
'-c',
'/work',
'sh',
'-c']
A one-shot prompt, --json and --tmux off stay in the current terminal:
The ramabana script, run_cli, makes the tmux decision before it imports the CLI. Importing the CLI takes about 0.4 seconds, so deciding first means the process outside tmux hands over to tmux quickly. launch_args(argv) reads just enough of the command line to know the session is interactive, without the full option parser, and returns the cfg and tmux values launch needs, or None to leave the decision to the CLI’s main. A prompt, --json, --doctor, an option refusal rejects, or any option it is unsure of returns None.
refusal holds the checks main makes before it starts anything, such as --pii without --vault, and returns the reason or ''. Running it here means a bad option is refused in your terminal, not on tmux’s hold screen. bare_resume gives a bare --resume the value PICK_SESSION, which lists this folder’s sessions to choose from. When run_cli has already called launch, it sets LAUNCHED so that main does not call it again.
The ramabana script: rerun inside tmux before anything heavy loads, then parse the command line for cli.main.
argv with a bare --resume, one followed by nothing or by another option, given the value PICK_SESSION.
~/.config/ramabana/config.toml holds this machine’s settings: one file, edited by hand, that every Ramabana on the machine reads. A session started with another --cfg, and an app that embeds Ramabana with a config folder of its own, read the same file and never write into their own folder for it. $RAMABANA_CONFIG names another file; the tests use it.
A value can come from four places, and the first that has one wins:
| Source | Example |
|---|---|
| a command-line flag | --model gpt-5.4 |
| an env var | $RAMABANA_MODEL, or $LEELA_MODEL inside Leela |
| config.toml | [jobs] turn = "gpt-5.4" |
| the built-in default | claude-opus-5-5 |
KEYS lists every key: its type, the env var and the flag that beat it, what it sets, and the values it may take. The tables are [jobs], the model each job runs on; [window], the numbers the window guard works with; [compaction]; [runtime], what a model is started with; and [cli], the defaults of the terminal session’s flags that take a value. [jobs] image is the OpenAI-compatible model generate_image draws with. [azure] holds Azure OpenAI’s endpoint, which $AZURE_OPENAI_ENDPOINT beats, read as named with no prefix (Key.plain), and the api_key_env that names the env var holding its key. A key an earlier release read, such as azure.deployments, is reported with what to do instead (RETIRED_KEYS). A [model."NAME"] table corrects the numbers fastllm’s table gives one model: max_input_tokens, max_output_tokens and supports_images. Given model_id, the same table defines a model of that name, with runtime, base_url, api_key_env, api_name, vendor_name and ctx; core registers it.
load_config reads the file again only when it changes. An entry it cannot use takes its default and gives one warning, which Agent.problems and ramabana config show repeat: a key nobody knows, with the nearest one it might mean; a value of the wrong type or outside its choices; a file that is not TOML. Nothing raises.
Set values in config.toml’s [table], adding the table when it is missing, comments kept; a file that is not TOML is refused, never rewritten.
Set top-level key in config.toml by rewriting its one line, comments kept. A missing file starts from config_template; one that is not TOML is refused, never rewritten.
The env vars that beat key, most specific first: its name as given for a plain key, else under each prefix.
The [model."NAME"] corrections for the first of names that has any, else {}.
Config.toml’s value for key, else dflt. An env var or a flag that beats it is read by the code that owns the key.
(values, problems) from config.toml, read again only when it changes. A key that is missing or wrong takes its default; nothing raises.
This machine’s config file: $RAMABANA_CONFIG, else CONFIG. An app that embeds ramabana reads the same file.
import tempfile, warnings
with tempfile.TemporaryDirectory() as d:
p = Path(d)/'config.toml'
p.write_text('learning = false\n[jobs]\nsummary = "gpt-5.4"\nturns = "x"\n[window]\nmargin = 0.5\n[model."codex/gpt-6.1-sol"]\nmax_input_tokens = 500000\nwindow = 3\n')
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter('always')
vals, bad = load_config(p)
test_eq(vals, {'learning': False, 'jobs.summary': 'gpt-5.4', 'model': {'codex/gpt-6.1-sol': {'max_input_tokens': 500000}}})
test_eq(len(bad), 3); test_eq(len(w), 3)
assert 'did you mean jobs.turn?' in bad[0] and 'margin = 0.5 is ignored: it must be a number, 1 or more' in bad[1]
p.write_text('[azure]\nendpoint = "https://r.openai.azure.com"\ndeployments = ["gpt-5.4"]\n[model."az"]\nmodel_id = "gpt-5.4"\nbase_url = "https://r.openai.azure.com"\nctx = 9\n')
with warnings.catch_warnings(record=True):
warnings.simplefilter('always')
vals, bad = load_config(p)
test_eq(vals, {'azure.endpoint': 'https://r.openai.azure.com', 'model': {'az': {'model_id': 'gpt-5.4', 'base_url': 'https://r.openai.azure.com', 'ctx': 9}}})
assert 'no longer choose models' in bad[0], bad
p.write_text('learning = [')
vals, bad = load_config(p)
test_eq(vals, {}); assert 'not valid TOML' in bad[0]
badUserWarning: /tmp/tmpxqop1clh/config.toml: it is not valid TOML (Invalid value (at end of document)), so every setting takes its default
vals, bad = load_config(p)
['/tmp/tmpxqop1clh/config.toml: it is not valid TOML (Invalid value (at end of document)), so every setting takes its default']
ramabana config: show what is in effect, init a commented config.toml, or print its path.
Each setting in effect and where it came from: an env var, config.toml or the built-in default. A flag is per command line, so it is named, not read.
A config.toml naming every key with its default, all commented out.
Every key’s built-in default, read from the module that owns it.
ramabana setup adds a provider when nothing runs, or another when you want one. It prints setup_guide from core, the providers and each job’s model, and asks which provider to add. Azure OpenAI, AWS Bedrock’s Mantle endpoint and any OpenAI-compatible server get a [model."NAME"] table (SETUP_FIELDS says what each asks); a provider whose models already have names, such as OpenAI or Claude Code, needs only the name. Last it asks which jobs the model serves and writes them into [jobs]. Flags answer the same questions without a terminal, and without a terminal or flags it prints the guide and exits 2. It writes with set_table, which edits or adds one table and keeps every comment, and never writes a key.
ramabana setup: add a provider’s model to config.toml and name the jobs it serves. Asks only on a terminal; never writes a key.
import tomllib
with tempfile.TemporaryDirectory() as d:
p = Path(d)/'config.toml'; p.write_text('learning = false # kept\n')
set_table('jobs', {'turn': 'az'}, p); set_table('az', {'model_id': 'gpt-5.4'}, p)
test_eq(tomllib.loads(p.read_text()), {'learning': False, 'jobs': {'turn': 'az'}, 'model': {'az': {'model_id': 'gpt-5.4'}}})
assert '# kept' in p.read_text()set_setting writes one top-level key, which only learning needs. It rewrites that key’s line, or the commented-out line the template left, so every comment stays. A missing file starts from the template. A file that is not valid TOML is refused with a message saying so, and never rewritten.
with tempfile.TemporaryDirectory() as d:
p = Path(d)/'config.toml'
test_eq(set_setting('learning', False, p), False)
text = p.read_text()
assert text.startswith(CONFIG_HEAD) and '\nlearning = false\n' in text and '# turn = ' in text
test_eq(load_config(p), ({'learning': False}, []))
set_setting('learning', True, p); test_eq(p.read_text().count('learning = '), 1)
p.write_text('# mine\nlearning = [')
try: set_setting('learning', False, p); raise AssertionError('rewrote a file it cannot read')
except ValueError as e: assert 'not valid TOML' in str(e)
test_eq(p.read_text(), '# mine\nlearning = [')
print(config_template()[:900])ramabana config reaches these before the session’s own options are parsed. ramabana config init writes the template, every key commented out beside its default, and refuses to replace a file that exists unless given --force, which keeps the old one as config.toml.bak and carries its learning across. ramabana config show prints each key’s value in effect and where it came from, with fastllm’s own number beside a corrected window. ramabana config path prints where the file is.
import os
with tempfile.TemporaryDirectory() as d:
os.environ['RAMABANA_CONFIG'] = str(p := Path(d)/'config.toml')
try:
test_eq(config_cli(['init']), 0); test_eq(config_cli(['init']), 1)
set_setting('learning', False)
test_eq(config_cli(['init', '--force']), 0)
assert (p.parent/'config.toml.bak').exists() and setting('learning') is False
p.write_text(p.read_text().replace('# summary = ', 'summary = "gpt-5.4" #', 1))
os.environ['RAMABANA_MODEL'] = 'gpt-5.4-mini'
out = config_show()
assert 'jobs.summary = "gpt-5.4" # config.toml' in out and 'jobs.turn = "gpt-5.4-mini" # $RAMABANA_MODEL' in out
assert 'jobs.classify = "claude-haiku-5-5" # built-in' in out
finally: os.environ.pop('RAMABANA_CONFIG'); os.environ.pop('RAMABANA_MODEL', None)
print(out)wrote /tmp/tmpm2bpoc3h/config.toml
wrote /tmp/tmpm2bpoc3h/config.toml
# /tmp/tmpm2bpoc3h/config.toml
learning = false # config.toml; --no-learned beats it
jobs.turn = "gpt-5.4-mini" # $RAMABANA_MODEL; --model beats it
jobs.oneshot = "gpt-6-luna" # built-in
jobs.inline = "" # built-in
jobs.completion = "claude-haiku-5-5" # built-in
jobs.classify = "claude-haiku-5-5" # built-in
jobs.summary = "gpt-5.4" # config.toml
jobs.subagent = "claude-sonnet-5-5" # built-in
jobs.image = "openai/gpt-image-1" # built-in
jobs.video = "gemini/veo-3.0-generate-001" # built-in
window.margin = 1.15 # built-in
window.image_tokens = 1600 # built-in
window.reserve = 16384 # built-in
window.keep_recent = 20000 # built-in
window.local_ctx = 0 # built-in
compaction.auto = true # built-in
compaction.strategy = "summary" # built-in
runtime.effort = "high" # built-in
runtime.tool_channel = "" # built-in
runtime.litert_backend = "" # built-in
azure.endpoint = "" # built-in
azure.api_key_env = "AZURE_OPENAI_API_KEY" # built-in
cli.approve = "ask" # built-in; --approve beats it
cli.theme = "auto" # built-in; --theme beats it
cli.tmux = "auto" # built-in; --tmux beats it
cli.pane = "auto" # built-in; --pane beats it
cli.profile = "auto" # built-in; --profile beats it
cli.max_tool_calls = "auto" # built-in; --max-tool-calls beats it
cli.max_steps = "auto" # built-in; --max-steps beats it
/tmp/tmpm2bpoc3h/config.toml exists; `ramabana config init --force` replaces it and keeps it as config.toml.bak
When tmux is missing, offer_install asks once whether to run the command that installs it, and shows the command: brew install tmux on macOS, sudo apt-get install -y tmux or sudo dnf install -y tmux on Linux. installer picks the command from what is on PATH. The command runs attached to the terminal, so sudo can ask for a password.
The answer is stored in <cfg>/setup.json. A no is never asked again, and neither is a yes whose install worked. Ctrl+c at the prompt, or an install that failed or was interrupted, stores nothing, so the next run asks again. ramabana --doctor asks even after an earlier no. Without tmux, the session starts in the current terminal and prints one line on how to get the now pane.
The first example says yes on a Mac with Homebrew, and the session relaunches in the tmux it installed. The second says no on Linux, and a second launch does not ask again:
Ask once, or again when force, to install tmux: True when tmux is there now.
When you start Ramabana inside a tmux server you already run, ext_keys reads that server’s extended-keys setting and turns it on only when it is off, printing a line to say so. Your ~/.tmux.conf is never changed. The example runs launch inside tmux with the setting off, on, and always, and only the first turns it on:
Turn on extended-keys in the tmux server we run inside when it is off: True if turned on.
The search_code tool searches a code index that kosha builds over the installed packages and the open folders. Rebuilding it, called a sync, takes time, so the session skips it when nothing it reads has changed.
index_gate(cfg, roots) decides this without importing kosha. It takes a fingerprint, index_state, of what a sync would read: the installed distributions that importlib.metadata finds, with their modification times (pkg_state), and for each folder its git HEAD and a digest of git status with the modification times of the paths it names (root_state). Outside git, a folder’s fingerprint is the newest modification time among its entries. It returns the fingerprint and a reason to run kosha, or '' to skip it. The reason is '' when the fingerprint matches the one the last good sync saved in <cfg>/kosha-state.json and each folder’s .kosha/code.db and kosha’s package database, kosha_env_db(), exist. Otherwise index_changes says what changed, such as packages changed: +foo 1.2.
After a good sync the session calls save_index to store the new fingerprint. Either way log_index appends one line to <cfg>/kosha.log, and --doctor shows the last line through last_index. The example walks a fresh folder through a first sync, a missing index, an unchanged state and a new package:
Append when · run|skip · why · secs to <cfg>/kosha.log, trimmed to its last LOG_KEEP lines past LOG_MAX bytes.
Keep state as the last good sync’s, beside the roots other sessions indexed.
(state, why): the fingerprint now, and why kosha must run (a missing code or env db included), or ’’ to skip it.
Why new differs from old, e.g. packages changed: +foo 1.2 or leela HEAD a1b2→c3d4; ’’ when it does not.
The fingerprint of what a sync of roots reads.
A root’s git HEAD and working-tree digest, staged changes included; outside git, the newest mtime of its entries.
{dist name-version: mtime} for what importlib.metadata finds on dirs, default sys.path, as kosha lists packages.
Where Kosha() keeps its package index: <xdg data>/kosha/env.db, as kosha computes it.
import tempfile
with tempfile.TemporaryDirectory() as d:
d = Path(d); (site := d/'site').mkdir(); (site/'fastcore-1.8.0.dist-info').mkdir(); (root := d/'proj').mkdir()
state, why = index_gate(d/'cfg', [root], [site])
test_eq((list(state['packages']), why), (['fastcore-1.8.0'], 'first sync'))
save_index(d/'cfg', state)
test_eq(index_gate(d/'cfg', [root], [site])[1], 'index missing')
(root/'.kosha').mkdir(); (root/'.kosha'/'code.db').write_text('')
if kosha_env_db().exists(): test_eq(index_gate(d/'cfg', [root], [site])[1], '')
(site/'foo-1.2.dist-info').mkdir()
assert index_gate(d/'cfg', [root], [site])[1].endswith('packages changed: +foo 1.2')
log_index(d/'cfg', 'skip', 'unchanged', .01)
test_eq(last_index(d/'cfg').split(' · ')[1:], ['skip', 'unchanged', '0.01s'])
Updating packages: 0%| | 0/31 [00:00<?, ?pkg/s]
--doctorramabana --doctor prints one line for each thing the now pane and the keys depend on: the tmux version, whether it can split a pane (tmux 3.1 or later), extended keys, the config file, ramabana-pane, the missing extras, and the code index’s last decision. When tmux is missing it then offers to install it, even after an earlier no. The example runs it on a fake machine with tmux 3.0, which is too old for the split:
Print one line per check, offer to install tmux when it is missing, and return 0.