# tools


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

A model can only write text. To read a file or run a command, it calls a
tool. A tool is a Python function. The backend turns its name, docstring
and typed parameters into a schema that the model sees. Ramabana’s tools
come from [shalya](https://github.com/vedicreader/shalya).

The tools act through a host. A host is the object that decides which
folders are open (its roots), whether files may be written and which
other services exist. Such services include a shell, a Python kernel,
git and the web. Each service is a capability group. A host declares a
group by inheriting the class that names it, such as `CodeHost` or
`WebHost`. `Host.provides` lists the groups a host declares. Every host
has the `file` group. The tools that need one capability group form a
tool group. The `shell` group, for example, is `run_shell`,
`run_shell_bg`, `shell_output` and `shell_stop`.

Shalya holds the host classes, the functions that build each tool group,
the conventions tool results follow (`err` writes a failure that
`failed` recognises), and the skills registry. This module re-exports
all of them. Code that imports them from `ramabana.tools` keeps working.
The alias `sys.modules[__name__] = ramabana.tools`, which Leela (the
notebook editor) uses, keeps resolving. New code imports them from
`shalya`.

What this module adds depends on parts of the agent that shalya does not
know. A sub-agent needs a
[`Backend`](https://vedicreader.github.io/ramabana/runtime.html#backend)
to talk to and a
[`Run`](https://vedicreader.github.io/ramabana/runtime.html#run) to
record it.
[`draws_itself`](https://vedicreader.github.io/ramabana/tools.html#draws_itself)
needs a
[`ModelSpec`](https://vedicreader.github.io/ramabana/core.html#modelspec).
The sections below cover a host that refuses everything, the image
tools,
[`tools_for`](https://vedicreader.github.io/ramabana/tools.html#tools_for),
which builds the tool list a turn gets, delegation to sub-agents, and
attachments.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/core.py#L245"
target="_blank" style="float:right; font-size:smaller">source</a>

### **getattr**

``` python
def __getattr__(
    name
):
```

*Resolve the deprecated Shalya compatibility surface without mirroring
it here.*

The re-exported names are shalya’s own objects, not copies. A copy could
drift into a second implementation. The cell below checks that every
name in `_all_` resolves and is the same object that shalya holds.
Shalya’s own notebooks test what the objects do.

``` python
len(_all_), sorted(_all_)[:8]
```

``` python
import shalya.core, shalya.host, shalya.skills, shalya.tools
here = sys.modules[__name__] if False else __import__('ramabana.tools', fromlist=['x'])
missing, different = [], []
for n in _all_:
    if not hasattr(here, n): missing.append(n); continue
    src = next((m for m in (shalya.core, shalya.host, shalya.skills, shalya.tools) if hasattr(m, n.lstrip('_')) or hasattr(m, n)), None)
    if src is None: continue
    theirs = getattr(src, n, None) or getattr(src, n.lstrip('_'), None)
    if getattr(here, n) is not theirs: different.append(n)
test_eq(missing, [])
test_eq(different, [])
test_eq((_cmds, _edits, _apply_edits, _diff),
        (shalya.core.cmds, shalya.core.edits, shalya.core.apply_edits, shalya.core.diff_text))
```

`WRITE_TOOLS` names every tool with an effect: tools that change files,
the repository, the kernel, the shell, the vault or a browser page. It
comes from shalya unchanged.

``` python
sorted(WRITE_TOOLS - shalya.core.WRITE_TOOLS)
```

``` python
assert {'replace_text', 'run_shell', 'git_commit'} <= WRITE_TOOLS
```

The examples below run against a real `LocalHost`, the host the terminal
uses, over a small project in a temporary folder. `index=False` skips
building the semantic code index. `provides` lists the capability groups
the host declares.

``` python
root = Path(tempfile.mkdtemp()).resolve()/'proj'
(root/'pkg').mkdir(parents=True)
(root/'pkg'/'sizes.py').write_text('def threshold(n):\n    "Half of n."\n    return n // 2\n')
local = LocalHost([root], index=False)
sorted(local.provides)
```

``` python
test_eq(local.can('file'), True)
```

## A host with nothing behind it

[`NullHost`](https://vedicreader.github.io/ramabana/tools.html#nullhost)
is a host with no folders and no services. Checking a path raises
`HostError`, reading returns nothing, and writing or deleting raises. It
declares no capability group beyond `file`. Its `writes` is False, and
[`tools_for`](https://vedicreader.github.io/ramabana/tools.html#tools_for)
gives it `view_file` and nothing else. Use it in a test, or wherever
code needs a host object that must not touch the disk. The examples
under it, and two further down, show each refusal.

The same cell adds `delete` and `exists` to `LocalHost`.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/tools.py#L90"
target="_blank" style="float:right; font-size:smaller">source</a>

### LocalHost.exists

``` python
def exists(
    path
):
```

*Whether `path` is a file inside the roots (an undecodable file
counts).*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/tools.py#L85"
target="_blank" style="float:right; font-size:smaller">source</a>

### LocalHost.delete

``` python
def delete(
    path
):
```

*Remove one file inside the roots; one already gone is not an error.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/tools.py#L69"
target="_blank" style="float:right; font-size:smaller">source</a>

### NullHost

``` python
def NullHost(
    roots:tuple=()
):
```

*A host with no folders and no services, which refuses every file
operation.*

``` python
test_eq([t.__name__ for t in tools_for(NullHost())], ['view_file'])
test_fail(lambda: NullHost().write('/a', 'x'))
```

## Drawing

Shalya’s `generate_image` tool makes pictures. Some models can draw
themselves when they are given the image tool. For any other model the
prompt goes to a separate image model. Which case applies is a fact
about the turn’s model, so shalya asks for it as a callable, and this
module supplies the callable.

[`draws_itself`](https://vedicreader.github.io/ramabana/tools.html#draws_itself)
looks up a model’s capabilities with
[`spec_caps`](https://vedicreader.github.io/ramabana/core.html#spec_caps)
and returns True when `image` is among the tools the model supports
(`Caps.tools`). The `supported_output_modalities` field cannot answer
this: it says no for every chat model, because it describes what a model
returns without being asked.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/tools.py#L107"
target="_blank" style="float:right; font-size:smaller">source</a>

### image_tools

``` python
def image_tools(
    host, mx:int=6000, session:str='', get_spec:NoneType=None, on_media:NoneType=None
):
```

*Shalya’s image tools for `host`, checking the turn’s model on every
call.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/tools.py#L96"
target="_blank" style="float:right; font-size:smaller">source</a>

### draws_itself

``` python
def draws_itself(
    spec
):
```

*Whether the model behind `spec` can draw pictures itself when given the
image tool.*

The example fakes the capability table with `mock_caps`, so it runs
without a model. The first spec lists `image` among its tools and the
second does not. No spec at all gives False.

``` python
from types import SimpleNamespace
draws = ModelSpec('drawer', 'remote', 'openai/gpt-5.6-luna')
plain = ModelSpec('plain', 'remote', 'openai/gpt-5.6')
caps = {'openai/gpt-5.6-luna': SimpleNamespace(tools=('image', 'search')), 'openai/gpt-5.6': SimpleNamespace(tools=('search',))}
with mock_caps(caps): got = draws_itself(draws), draws_itself(plain), draws_itself(None)
got
```

    (True, False, False)

``` python
with mock_caps(caps):
    test_eq(draws_itself(draws), True)
    test_eq(draws_itself(plain), False)
test_eq(draws_itself(None), False)
with mock_caps({}): test_eq(draws_itself(draws), False)
```

[`image_tools`](https://vedicreader.github.io/ramabana/tools.html#image_tools)
builds shalya’s image group for a host, with
[`draws_itself`](https://vedicreader.github.io/ramabana/tools.html#draws_itself)
asked about the turn’s model each time the tool is called. Pass
`get_spec`, a callable returning the current
[`ModelSpec`](https://vedicreader.github.io/ramabana/core.html#modelspec).
The tool then sees a model switch mid-session. Without an image API key
the tool returns a message saying so and makes no request. This example
therefore runs anywhere.

``` python
gen = image_tools(local, get_spec=lambda: draws)[0]
gen.__name__, image_available()
```

``` python
if not image_available(): assert failed(gen('a kettle'))
assert failed(gen('a kettle', size='3x3'))
test_eq(len(image_tools(local, get_spec=lambda: None)), 1)
```

## Assembling the tool list

[`tools_for`](https://vedicreader.github.io/ramabana/tools.html#tools_for)
decides which tools a host gets. It asks `Host.provides` for the
capability groups and adds the tool group for each. A `get_skills`
callable adds `read_skill`, and `extra` adds extension tools. `drop`
removes groups by name, such as `'shell'` or `'image'`. `optin` adds
groups that are off by default: `exhash` (the hash-addressed
`edit_file`), `research`, `author`, `legacy` (tool names from older
releases) and `browser`.

Shalya has a
[`tools_for`](https://vedicreader.github.io/ramabana/tools.html#tools_for)
of its own. Use this one in Ramabana code, because it also builds the
image group from the turn’s model through `get_spec` and `on_media`,
which a host cannot know. The image group comes only when an image key
is set and the host can write, since `generate_image` saves files. On a
`LocalHost` without litesearch installed, `search_code` is left out.

[`small_tool`](https://vedicreader.github.io/ramabana/tools.html#small_tool)
marks an extension tool to keep in the `small` profile, the short
catalog a local model or a small context window gets.
[`ToolCatalog`](https://vedicreader.github.io/ramabana/tools.html#toolcatalog)
wraps a tool list once and answers questions about it: each tool’s
group, which tools write, which have effects, and a read-only copy.
[`ToolEntry`](https://vedicreader.github.io/ramabana/tools.html#toolentry)
is one row of it.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/tools.py#L147"
target="_blank" style="float:right; font-size:smaller">source</a>

### ToolCatalog

``` python
def ToolCatalog(
    tools:tuple=()
):
```

*An immutable list of tools that answers each one’s group, whether it
writes, and whether it has effects.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/tools.py#L131"
target="_blank" style="float:right; font-size:smaller">source</a>

### ToolEntry

``` python
def ToolEntry(
    tool:object, group:str=''
)->None:
```

*One callable and the Shalya metadata every consumer needs.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/tools.py#L125"
target="_blank" style="float:right; font-size:smaller">source</a>

### small_tool

``` python
def small_tool(
    f
):
```

*Mark an extension tool for the small profile, whose catalog omits
unmarked tools.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/tools.py#L115"
target="_blank" style="float:right; font-size:smaller">source</a>

### tools_for

``` python
def tools_for(
    host, get_skills:NoneType=None, extra:tuple=(), mx:int=6000, drop:tuple=(), get_spec:NoneType=None,
    on_media:NoneType=None, optin:tuple=()
):
```

*The tools for `host`: a group per capability it declares, plus skills,
images and extensions, less `drop` and plus `optin`.*

The list for the real host above. It has the file, code, notebook,
shell, git and web groups. The memory and API tools are absent because
`LocalHost` does not declare those groups here, and `generate_image`
appears only when an image key is set.

``` python
sorted(t.__name__ for t in tools_for(local))
```

``` python
ts = tools_for(local)
names = {t.__name__ for t in ts}
assert {'search_code', 'view_file', 'run_python', 'run_shell'} <= names, sorted(names)
assert not (names & {'memory_tree', 'api_load', 'ask_memory'})
test_eq('generate_image' in names, image_available())
test_eq(names - {t.__name__ for t in tools_for(local, drop=['shell'])},
        {'run_shell', 'run_shell_bg', 'shell_output', 'shell_stop'})
assert 'read_skill' in {t.__name__ for t in tools_for(local, get_skills=lambda: [])}
assert 'read_skill' not in names
_installed, installed = installed, lambda m: m != 'litesearch'
try: assert 'search_code' not in {t.__name__ for t in tools_for(local)}
finally: installed = _installed
```

Opt-in groups come only on request. `edit_file` appears with
`optin=('exhash',)`, the old tool names with `optin='legacy'`, and an
unknown name raises.

``` python
assert 'edit_file' not in names
assert 'edit_file' in {t.__name__ for t in tools_for(local, optin=('exhash',))}
assert 'list_files' in {t.__name__ for t in tools_for(local, optin='legacy')}
test_fail(lambda: tools_for(local, optin=('nosuch',)))
```

``` python
h = NullHost(['/proj'])
sorted(h.provides), [t.__name__ for t in tools_for(h)]
```

``` python
test_fail(lambda: h.check('a.py'))
```

Every method of
[`NullHost`](https://vedicreader.github.io/ramabana/tools.html#nullhost)
refuses. Checking a path raises because no folders are open, reading
gives nothing, and opening another folder raises. It is the reference
for how a host says “I cannot”.

``` python
h.walk(), h.read('a.py'), h.text_at('a.py')
```

``` python
test_eq(h.walk(), [])
test_eq(h.read('a.py'), None)
test_eq(h.text_at('a.py'), None)
test_fail(lambda: h.write('a.py', 'x'))
test_fail(lambda: h.check('a.py'))
test_fail(lambda: NullHost().add_root('/tmp'))
```

## Sub-agents

A sub-agent is a separate conversation with a model, started to answer
one question for the agent that asked. Handing a question over this way
is delegation. Delegation saves context. It does not save time. A broad
question can take twenty tool calls to answer. If the main agent made
them itself, every result would stay in its conversation. Delegated, the
same work costs the main conversation one question and one answer. The
sub-agent’s conversation is thrown away when it finishes.

A sub-agent gets read-only tools unless the session allows it to write.
`NO_SUB` lists the tools it never gets by default: the five delegation
tools and the two watch tools (`watch` and `cancel_watch`). A watch
would outlive the sub-agent’s task. A session that allows nesting passes
its own `block`. The sub-agent may then delegate in turn. These
constants set the limits:

<table>
<colgroup>
<col style="width: 33%" />
<col style="width: 33%" />
<col style="width: 33%" />
</colgroup>
<thead>
<tr>
<th>Constant</th>
<th>Value</th>
<th>Meaning</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>SUB_MAX_STEPS</code></td>
<td>80</td>
<td>Steps a delegation gets when it names none.</td>
</tr>
<tr>
<td><code>SUB_STEP_CEILING</code></td>
<td>400</td>
<td>Most steps a delegation may ask for under <code>auto</code>, and the
most a numeric session cap may be.</td>
</tr>
<tr>
<td><code>SUB_MIN_STEPS</code></td>
<td>20</td>
<td>Fewest steps a delegation runs with when it asks for fewer.</td>
</tr>
<tr>
<td><code>SUB_TIMEOUT</code></td>
<td>1800</td>
<td>Seconds before a delegation is stopped. 0 never stops it.</td>
</tr>
<tr>
<td><code>SUB_STEP_SECONDS</code></td>
<td>20</td>
<td>Under <code>auto</code> steps, a delegation with the default time
limit gets its steps times this many seconds, when that is more than
<code>SUB_TIMEOUT</code>.</td>
</tr>
<tr>
<td><code>SUB_TIMEOUT_CEILING</code></td>
<td>10800</td>
<td>Most seconds that step-based limit can reach.</td>
</tr>
<tr>
<td><code>SUB_UNFINISHED</code></td>
<td>300</td>
<td>A reply with no tool call and no <code>state:</code> line, shorter
than this many characters, counts as a progress line and not a
report.</td>
</tr>
<tr>
<td><code>SUB_SLOTS</code></td>
<td>4</td>
<td>Sub-agents working at once across one agent’s whole delegation
tree.</td>
</tr>
</tbody>
</table>

A briefing is the standing instructions a model gets before the
conversation starts, its system prompt.
[`sub_briefing`](https://vedicreader.github.io/ramabana/tools.html#sub_briefing)
builds a sub-agent’s briefing from parts in this order: `SUB_SP_HEAD`,
which every sub-agent gets, then `SUB_READ_SP` or `SUB_WRITE_SP`, then
an optional note on how much of the answer the caller will read, then
notes for the model’s family.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/tools.py#L210"
target="_blank" style="float:right; font-size:smaller">source</a>

### sub_briefing

``` python
def sub_briefing(
    writes:bool=False, answer_max:int=0, # characters the caller reads of the answer; 0 says nothing about it
    notes:str='', # the sub-agent model's family notes, after the rest
):
```

*The sub-agent standing instructions: the shared half, the read-only or
the write half, the answer budget and the family notes.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/tools.py#L202"
target="_blank" style="float:right; font-size:smaller">source</a>

### inbox_note

``` python
def inbox_note(
    key
):
```

*The one tag the model may take as the user: it carries `key`, which no
file or page knows.*

`SUB_SP` is the read-only briefing with nothing added, and it is the
default. A sub-agent allowed to write gets `SUB_SP_HEAD` followed by
`SUB_WRITE_SP`.

``` python
SUB_MAX_STEPS, len(SUB_SP.split()), len(SUB_WRITE_SP.split())
```

``` python
assert SUB_MIN_STEPS < SUB_MAX_STEPS
```

`read_only`, from shalya, filters a tool list down to what an agent may
have when it must not change anything. Write tools are dropped, or
swapped for a safe version where the tool has one. `block` drops tools
by name, and Ramabana passes `NO_SUB`. `effects=False` also drops the
acting tools, those that do something without writing a file, such as
`inspect_python`, `shell_stop` and `generate_image`. Shalya marks them
with `acts` and lists them in `ACTING_TOOLS`. Use `effects=False` for a
surface that may look and propose but never act.

A safe version can fix an argument. In the second cell below, `read_url`
still fetches the page, but its `remember` parameter is gone and it no
longer stores the page in the vault.

``` python
[t.__name__ for t in read_only(ts, block=NO_SUB)]
```

``` python
import inspect
```

``` python
test_eq(set(t.__name__ for t in read_only(ts, block=NO_SUB)) & WRITE_TOOLS, set())
shut = set(t.__name__ for t in read_only(ts, effects=False, block=NO_SUB))
test_eq(shut & (WRITE_TOOLS | NO_SUB | ACTING_TOOLS), set())
test_eq('search_code' in shut, True)

kept = []
class Page: text = 'page'
class Keeps(MemHost, WebHost):
    def web_search(self, query, n=20): return []
    def research(self, query): return ''
    def read_url(self, url, remember=True): kept.append(remember); return Page()
fetch = next(t for t in read_only(tools_for(Keeps({'/proj/a.py': 'x = 1\n'}))) if t.__name__ == 'read_url')
fetch(url='https://example.com')
test_eq(kept, [False])
test_eq('remember' in inspect.signature(fetch).parameters, False)
sorted(set(t.__name__ for t in ts) & ACTING_TOOLS)
```

`max_calls` puts a hard budget on the list. Past it, every tool returns
a message telling the model to stop and answer. Local engines run their
own tool loop. Ramabana cannot stop them between steps. A budget inside
the tools is the one limit that works on every backend. Below, the
second search is refused.

``` python
budgeted = read_only(ts, max_calls=1, block=NO_SUB)
search = next(t for t in budgeted if t.__name__ == 'search_code')
search('return'), search('return')
```

[`delegate`](https://vedicreader.github.io/ramabana/tools.html#delegate)
asks one question of a sub-agent. It spawns a new conversation on the
same engine as `backend`, hands it the tools that `read_only` lets
through (with `writes=True`, the write tools too), sends the question,
and closes the conversation in a `finally` block whether or not it
succeeded. The tools carry a call budget of four times `max_steps`. A
short reply with no tool call and no `state:` line gets one nudge
(`SUB_NUDGE`), and the sub-agent answers again.
[`delegate`](https://vedicreader.github.io/ramabana/tools.html#delegate)
returns the answer text. An empty or repetitive answer is replaced by a
notice that the delegation failed. A sub-agent that raised gives an
error result. `timeout` cancels the run after that many seconds, and
`slots`, a shared
[`Slots`](https://vedicreader.github.io/ramabana/tools.html#slots),
makes it wait for a free place among the sub-agents working at once. A
sub-agent waiting on sub-agents of its own lends its place back. A full
tree of delegations then cannot deadlock.

Use
[`delegate`](https://vedicreader.github.io/ramabana/tools.html#delegate)
for one question.
[`delegate_many`](https://vedicreader.github.io/ramabana/tools.html#delegate_many),
below, asks several, and
[`subagent_tools`](https://vedicreader.github.io/ramabana/tools.html#subagent_tools)
wraps both as tools a model can call.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/tools.py#L360"
target="_blank" style="float:right; font-size:smaller">source</a>

### delegate

``` python
def delegate(
    backend, question, tools:tuple=(), sp:NoneType=None, max_steps:int=80, skills:tuple=(),
    writes:bool=False, # hand over WRITE_TOOLS as well
    approve:NoneType=None, # the gate those writes answer to
    run:NoneType=None, # a pre-registered child run
    images:tuple=(), # picture bytes sent with the question
    block:frozenset=frozenset({'delegate_tell', 'delegate_cancel', 'delegate_search', 'delegate_async', 'delegate_result', 'watch', 'cancel_watch'}), # tool names withheld from the sub-agent
    timeout:int=0, # seconds before the run is cancelled; 0 never
    slots:NoneType=None, # a shared `Slots`, held while the sub-agent works
    spawn:NoneType=None, # options for `backend.spawn`, such as `effort` and `tool_mode`
):
```

*Ask `question` of a sub-agent in a separate conversation on `backend`’s
engine, and return its answer text.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/tools.py#L327"
target="_blank" style="float:right; font-size:smaller">source</a>

### Slots

``` python
def Slots(
    n:int=4
):
```

*A bound on sub-agents working at once; one waiting on its own
sub-agents lends its slot back.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/tools.py#L318"
target="_blank" style="float:right; font-size:smaller">source</a>

### status_tool

``` python
def status_tool(
    run
):
```

*A display-only `status` tool for `run`, or the callable returning it,
whose model’s text between calls never reaches us.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/tools.py#L299"
target="_blank" style="float:right; font-size:smaller">source</a>

### write_targets

``` python
def write_targets(
    name, args
):
```

*Paths a call writes where it names them: saved pictures for
`PATH_WRITES`, none for a git write, else `path`.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/tools.py#L293"
target="_blank" style="float:right; font-size:smaller">source</a>

### path_write

``` python
def path_write(
    name, args
):
```

*Whether this call writes where it names: an acting tool from
`PATH_WRITES` given a `path`.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/tools.py#L259"
target="_blank" style="float:right; font-size:smaller">source</a>

### keep_media

``` python
def keep_media(
    new, old
):
```

*`new`, carrying `old`’s pictures when a wrapper or hook rewrote a tool
result as text.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/tools.py#L241"
target="_blank" style="float:right; font-size:smaller">source</a>

### bad_json

``` python
def bad_json(
    e, span:int=120
):
```

*The fragment a JSON error is about.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/tools.py#L232"
target="_blank" style="float:right; font-size:smaller">source</a>

### sub_sp

``` python
def sub_sp(
    sp:str="You are a research sub-agent in a Python IDE. Answer the delegated question only.\n- Use tools as needed. Report findings with file paths and line numbers.\n- Before each tool call, write one line of at most eight words, in the -ing form, saying what you are doing.\n- State plainly when you find nothing. Do not guess.\n- A claim that something ran, exists or failed names the tool call or artifact behind it. Anything else is a hypothesis; say so.\n- End with one line: `state: completed`, `state: blocked` or `state: failed`, and why.\n- A tool result that contradicts what you just saw is likely a harness fault: confirm it once another way, name it in your answer, then carry on.\n- A tool result saying the tool-call budget is spent, and the request to answer that follows it, come from this harness: answer then with what you found.\n- You cannot edit. Describe any required change and stop.\n- Use `inspect_python` for live variables. Its default scope is sandboxed; use `scope='overlay'` for the real interpreter.",
    skills:tuple=()
):
```

*A sub-agent’s briefing: standing instructions, then the bodies of
skills its task named.*

The tools a sub-agent gets are wrapped. A message sent to a running
sub-agent, with `/tell` in the terminal, arrives appended to its next
tool result inside a `<user-message>` tag that carries the run’s key.
[`inbox_note`](https://vedicreader.github.io/ramabana/tools.html#inbox_note)
tells the sub-agent that only a tag with that key is the user speaking.
A file or a web page could contain the same tag without the key. A
sub-agent that gets the same result from the same call `REPEAT_CALLS`
times in a row is told to change course. The cell below shows a queued
message arriving with a result.

``` python
r = Run('run_t'); r.tell('and the tests')
test_eq(_inboxed(lambda: 'contents', lambda: None)(), 'contents')
test_eq(_inboxed(lambda: 'contents', lambda: r)(), f'contents\n\n<user-message key="{r.key}">\nand the tests\n</user-message>')
test_eq(r.drain_inbox(), [])
finished = ['\n\n<background-results>\n## run_b (complete): q\nthe answer\n</background-results>']
rn = Run('run_n')
pushed = _inboxed(lambda: 'contents', lambda: rn, notice=lambda: finished.pop() if finished else '')
test_eq(pushed(), 'contents' + '\n\n<background-results>\n## run_b (complete): q\nthe answer\n</background-results>')
test_eq(pushed(), 'contents')   # a finished answer arrives once
```

Each wrapped call also updates the run’s status
([`Run.on_call`](https://vedicreader.github.io/ramabana/runtime.html#run.on_call)).
The now pane, the side panel that lists live runs (see
[pane](18_pane.ipynb)), shows what a sub-agent is doing from that
status. Some backends, LiteRT for one, never pass on the text a model
writes between calls. Their sub-agents get a `status` tool from
[`status_tool`](https://vedicreader.github.io/ramabana/tools.html#status_tool)
to report one line. `STATUS_NOTE` in the briefing tells them to use it.

``` python
class _Quiet(FakeBackend):
    narrates = False
    def spawn(self, sp='', tools=(), **kw):
        self.sub = FakeBackend(self.spec, sp=sp, tools=tools, shared=True)
        self.sub._send = lambda msg, **kw: next(t for t in self.sub.tools if t.__name__ == 'status')(text='Reading the tests')
        return self.sub

qb, qr = _Quiet(), Run('run_quiet', 'child', 'q')
delegate(qb, 'q', tools=ts, run=qr)
test_eq(qr.status, 'Reading the tests')
assert STATUS_NOTE in qb.sub.sp
```

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/tools.py#L433"
target="_blank" style="float:right; font-size:smaller">source</a>

### delegate_many

``` python
def delegate_many(
    backend, questions, tools:tuple=(), sp:NoneType=None, max_steps:int=80, n_workers:int=4, skills:tuple=(),
    writes:bool=False, approve:NoneType=None, parent:NoneType=None, images:tuple=(),
    block:frozenset=frozenset({'delegate_tell', 'delegate_cancel', 'delegate_search', 'delegate_async', 'delegate_result', 'watch', 'cancel_watch'}),
    timeout:int=0, slots:NoneType=None,
    spawn:NoneType=None, # `spawn` options for every question, or a list with one per question
):
```

*Ask several questions of one sub-agent each, and return the answers in
question order. `backend`, `sp` and `spawn` may be lists, one per
question, and `tools` a callable from a backend’s spec to its tools; see
[`lanes`](https://vedicreader.github.io/ramabana/tools.html#lanes) for
what runs at once.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/tools.py#L426"
target="_blank" style="float:right; font-size:smaller">source</a>

### lanes

``` python
def lanes(
    backends, writes:bool=False
):
```

*Question indices grouped into what runs one after another: every write
in one lane, the questions on a local engine in one, each cloud question
alone.*

Here a fake backend answers a delegated question. The caller gets back
only the answer text.

``` python
be = FakeBackend(replies=['the caller never sees this'])
delegate(be, 'which files import fastllm?', tools=ts)
```

``` python
from urai import ToolCallError
```

``` python
# a sub-agent whose tool call was cut off reports why in its answer
class _Cut(FakeBackend):
    def spawn(self, sp='', tools=(), **kw):
        s = super().spawn(sp, tools, **kw)
        def _send(msg, **kw): raise ToolCallError("openai/gpt-5.6's tool call was cut off at its 16384-token output limit")
        s._send = _send
        return s
out = delegate(_Cut(), 'edit the notebook', tools=ts)
assert 'tool call was cut off' in out and 'JSONDecodeError' not in out, out
```

The spawned conversation was separate. It held only the question and the
answer, and
[`delegate`](https://vedicreader.github.io/ramabana/tools.html#delegate)
closed it before returning.

``` python
test_eq(len(be.spawned), 1)
be.spawned[0].hist
```

[`delegate_many`](https://vedicreader.github.io/ramabana/tools.html#delegate_many)
asks several questions at once, one sub-agent each, and returns the
answers in the order the questions were asked. It registers every child
run with the parent before any starts. Cancelling the parent then
reaches all of them. The last example on this page shows that. Questions
to cloud models run in parallel, up to `n_workers` (default 4) at once.
Questions run one at a time when sub-agents may write, and questions to
local models run one at a time. An engine such as LiteRT holds one
conversation at a time, and parallel runs would race for it.

``` python
delegate_many(be, ['what imports fastllm?', 'where is compaction triggered?'], tools=ts)
```

The delegation tools take a callable that returns a backend. Each call
asks for the current session’s backend. After a `/model` switch the next
sub-agent runs on the new model.

`delegate_async` starts a sub-agent in the background and returns at
once. The turn can carry on while the sub-agent works.
[`Background`](https://vedicreader.github.io/ramabana/tools.html#background)
keeps track of those runs. `start` registers a run before its thread
begins. A status call therefore always finds the id that `start`
returned. At most `mx` runs reach a model at once (`ASYNC_MAX`, 4 by
default) and the rest wait as
[`pending`](https://vedicreader.github.io/ramabana/learned.html#pending).
`status`, `result` and `cancel` answer for one run, and `status` with no
id answers for all. The last `keep` answers are held. `close`, called
when the session ends, cancels every run and refuses new work.

The examples hold two runs behind a
[`Background`](https://vedicreader.github.io/ramabana/tools.html#background)
with one place, cancel a run still waiting, close a
[`Background`](https://vedicreader.github.io/ramabana/tools.html#background),
catch a run that raised, and show an answer dropped once newer ones push
it out.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/tools.py#L472"
target="_blank" style="float:right; font-size:smaller">source</a>

### Background

``` python
def Background(
    mx:int=4, # how many reach a model at once
    keep:int=50, # answers held before the oldest is dropped
    on_done:NoneType=None, # called with `(run, answer)` when a run finishes
):
```

*Delegations running behind the turn that asked for them, and the
answers they leave.*

``` python
bg = Background(mx=1)
gate, seen = threading.Event(), []

def slow(run):
    run.start()
    seen.append(run.id)
    gate.wait(5)
    return f'answered {run.question}'

a = bg.start(slow, Run('run_a', 'child', 'first'))
b = bg.start(slow, Run('run_b', 'child', 'second'))
time.sleep(.2)
[(r['id'], r['state']) for r in bg.status()]
```

``` python
test_eq({r['id'] for r in bg.status()}, {'run_a', 'run_b'})
test_eq(bg.status('run_a')[0]['state'], 'running')
test_eq(bg.status('run_b')[0]['state'], 'pending')
test_eq(len(seen), 1)

gate.set()
for _ in range(200):
    if bg.result('run_b').startswith('answered'): break
    time.sleep(.02)
test_eq(bg.result('run_a'), 'answered first')
test_eq(bg.result('run_b'), 'answered second')
test_eq(len(seen), 2)
```

``` python
held = Background(mx=1)
block, ran = threading.Event(), []
held.start(lambda r: (r.start(), block.wait(5), ran.append(r.id))[2], Run('run_1', 'child', 'holds the slot'))
held.start(lambda r: ran.append(r.id), Run('run_2', 'child', 'never runs'))
time.sleep(.2)
held.cancel('run_2')
block.set()
for _ in range(200):
    if 'stopped' in held.result('run_2'): break
    time.sleep(.02)
test_eq(ran, ['run_1'])
```

``` python
shut = Background()
shut.start(lambda r: 'done', Run('run_x', 'child', 'q'))
test_eq(shut.close(), 1)
test_fail(lambda: shut.start(lambda r: 'nope', Run('run_y', 'child', 'q')))
assert 'run_y' not in {r['id'] for r in shut.status()}

boom = Background()
boom.start(lambda r: 1/0, Run('run_z', 'child', 'q'))
for _ in range(200):
    if ERR in boom.result('run_z'): break
    time.sleep(.02)
assert 'ZeroDivisionError' in boom.result('run_z'), boom.result('run_z')

small = Background(keep=1)
for n in ('run_p', 'run_q'): small.start(lambda r: f'ans {r.id}', Run(n, 'child', 'q'))
for _ in range(200):
    if small.result('run_q') == 'ans run_q': break
    time.sleep(.02)
assert small.result('run_p') != 'ans run_p', small.result('run_p')
```

`result(run_id, wait)` waits up to `wait` seconds for the answer, at
most `WAIT_MAX`. A run that has finished but has not yet saved its
answer gets `SAVE_WAIT` seconds. While a run works, `result` says that
the answer comes by itself, so the model does not poll.

``` python
slow, go = Background(), threading.Event()
slow.start(lambda r: (go.wait(5), 'slow answer')[1], Run('run_s', 'child', 'q'))
assert 'Do not ask again' in slow.result('run_s') and 'ask again later' not in slow.result('run_s')
threading.Timer(.2, go.set).start()
test_eq(slow.result('run_s', wait=5), 'slow answer')
```

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/tools.py#L609"
target="_blank" style="float:right; font-size:smaller">source</a>

### subagent_tools

``` python
def subagent_tools(
    get_backend, get_tools, get_skills:NoneType=None, get_cloud_backend:NoneType=None,
    get_writes:NoneType=None, # the session's sub-agent write toggle, read per call
    get_approve:NoneType=None, # the gate those writes answer to
    background:NoneType=None, # the `Background` for async delegations; made if None
    get_log_dir:NoneType=None, # callable -> the folder run transcripts go in, or None
    get_pictures:NoneType=None, # callable paths -> `(bytes, err)`; see `read_pictures`
    get_steps:NoneType=None, # callable -> the step budget, which also caps a call's `max_steps`
    get_timeout:NoneType=None, # callable -> seconds each delegation may run; 0 never stops it
    get_max:NoneType=None, # callable -> chars one result may spend, notes included; `MAX_TOOL_CHARS` when None
    get_notes:NoneType=None, # callable spec -> the briefing notes for that model's family, or ''
    slots:NoneType=None, # a shared `Slots` bounding sub-agents at once
    block:frozenset=frozenset({'delegate_tell', 'delegate_cancel', 'delegate_search', 'delegate_async', 'delegate_result', 'watch', 'cancel_watch'}), # names withheld from the sub-agent, when `get_tools` does not decide
    get_sub:NoneType=None, # callable (model, effort) -> `(backend, spawn options, note)`, raising why it cannot; None uses `get_backend` and `get_cloud_backend`
    get_tools_for:NoneType=None, # callable spec -> the tools for a sub-agent on that model; None gives every sub-agent `get_tools()`
):
```

*The five delegation tools. The callable arguments are read on each
call. The tools follow the session’s current settings.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/tools.py#L600"
target="_blank" style="float:right; font-size:smaller">source</a>

### sub_steps

``` python
def sub_steps(
    n:int=0, setting:str='auto'
):
```

*Resolve a delegation step request against the session setting.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/tools.py#L589"
target="_blank" style="float:right; font-size:smaller">source</a>

### renamed

``` python
def renamed(
    **old
):
```

*A tool that also takes each keyword `old` names, as the one it now
names; its schema shows only the new.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/tools.py#L574"
target="_blank" style="float:right; font-size:smaller">source</a>

### read_pictures

``` python
def read_pictures(
    host, paths
):
```

*`(bytes, '')` for pictures inside the host’s roots, or `([], err)` for
the first one refused.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/tools.py#L567"
target="_blank" style="float:right; font-size:smaller">source</a>

### picture_mime

``` python
def picture_mime(
    head
):
```

*The MIME of a PNG, JPEG, GIF or WebP from its first bytes, else None.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/tools.py#L551"
target="_blank" style="float:right; font-size:smaller">source</a>

### named_skills

``` python
def named_skills(
    get_skills, names
):
```

*The skills a delegated task named, and a note about any name that
matched nothing.*

A skill is a markdown document of instructions that an agent reads when
the work calls for it, and a delegated task can name the skills its
sub-agent needs.
[`named_skills`](https://vedicreader.github.io/ramabana/tools.html#named_skills)
finds them by name. When a name matches nothing, it returns a note that
lists the skills that do exist. It does not raise. The model can then
correct the name.
[`sub_sp`](https://vedicreader.github.io/ramabana/tools.html#sub_sp)
appends the full text of the found skills to the sub-agent’s briefing.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/tools.py#L841"
target="_blank" style="float:right; font-size:smaller">source</a>

### parse_plan_items

``` python
def parse_plan_items(
    items
):
```

*The item texts from newline text, a JSON list, or a Python list.*

[`parse_plan_items`](https://vedicreader.github.io/ramabana/tools.html#parse_plan_items)
turns a list a model sent into a list of strings. Models send a list as
JSON text, as one item per line (with or without `-` or `*` bullets), or
as a real list. The function accepts all three. The agent’s `set_plan`
tool reads its todo items through it.

The next cell returns to skills. It asks
[`named_skills`](https://vedicreader.github.io/ramabana/tools.html#named_skills)
for one skill that exists and one that does not. It then checks what
[`sub_sp`](https://vedicreader.github.io/ramabana/tools.html#sub_sp)
adds to the briefing.

``` python
skills = [Skill('house-style', 'md', 'How code is written here.', _text='Dense lines.'),
          Skill('deploy', 'md', 'How this ships.', _text='Push the tag.')]
named_skills(lambda: skills, 'house-style'), named_skills(lambda: skills, 'nope')
```

``` python
got, note = named_skills(lambda: skills, 'house-style')
test_eq([s.name for s in got], ['house-style'])
test_eq(note, '')
test_eq([s.name for s in named_skills(lambda: skills, 'house-style,deploy')[0]],
        ['house-style', 'deploy'])
test_eq(named_skills(lambda: skills, ''), ([], ''))
missed, note = named_skills(lambda: skills, 'nope')
test_eq(missed, [])
assert note

briefed = sub_sp(skills=got)
assert 'Dense lines.' in briefed and briefed.startswith(SUB_SP)
assert 'Push the tag.' not in briefed
test_eq(sub_sp(), SUB_SP)
```

A pyskill is a skill shipped as a Python module whose docstring is the
skill text. Listing skills needs each one’s description, and shalya
imports the module to read it. Some of those imports are slow:
`vishalakshi.skill` and `kosha.skill` pull in litesearch and model2vec,
about 0.7 seconds before the first prompt.
[`mod_doc`](https://vedicreader.github.io/ramabana/tools.html#mod_doc)
reads a module’s docstring from its source file without importing
anything, and this cell swaps shalya’s pyskill loader for one that uses
it. The module is imported only when the skill’s full text is read.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/tools.py#L858"
target="_blank" style="float:right; font-size:smaller">source</a>

### mod_doc

``` python
def mod_doc(
    modpath
):
```

*A module’s docstring read from its source, loading nothing; None when
that cannot be done.*

``` python
test_eq(mod_doc('ramabana.theory'), sys.modules['ramabana.theory'].__doc__ if 'ramabana.theory' in sys.modules else __import__('ramabana.theory').theory.__doc__)
assert mod_doc('no_such.module') is None and 'no_such' not in sys.modules
```

[`subagent_tools`](https://vedicreader.github.io/ramabana/tools.html#subagent_tools)
builds the five tools a model uses to delegate. `delegate_search` asks
one or more questions and waits for the answers. `delegate_async` starts
one question in the background and returns a run id. `delegate_result`
collects a background answer, or with no id lists every background run,
`delegate_cancel` stops one, and `delegate_tell` sends a running one a
message. Most arguments are callables that
[`subagent_tools`](https://vedicreader.github.io/ramabana/tools.html#subagent_tools)
reads on each call. The backend, the step budget, the time limit and the
write setting follow the session as they change. An answer longer than
the tool-result limit is cut. The whole text is saved to a file, and the
cut answer names the path.

``` python
subs = {t.__name__: t for t in subagent_tools(lambda: be, lambda: ts)}
delegate_search = subs['delegate_search']
list(subs)
```

With no model to delegate to, the tools return a message saying so
instead of raising into the turn. With a model, several questions come
back with one heading each.

``` python
nomodel = {t.__name__: t for t in subagent_tools(lambda: None, lambda: ts)}
assert isinstance(nomodel['delegate_search'](['anything']), str)
assert isinstance(nomodel['delegate_async']('anything'), str)
delegate_search(['what imports fastllm?', 'which tests cover it?'])
```

``` python
from fastcore.funccall import get_schema
```

``` python
dt = {t.__name__: t for t in subagent_tools(lambda: be, lambda: [], None, background=Background())}
assert 'sub answer' in dt['delegate_search'](['one question'])
out = dt['delegate_search'](['q1', 'q2']); test_eq(out.count('### '), 2)
n0 = len(be.spawned)
dt['delegate_search']([]); dt['delegate_search'](['', '  '])
test_eq(len(be.spawned), n0)
assert 'sub answer' in dt['delegate_search']('a bare string still works')
test_eq(dt['delegate_search']('["q1", "q2"]').count('### '), 2)
test_eq(dt['delegate_search']('one question\nthat runs onto a second line').count('### '), 0)
rid = dt['delegate_async']('later').split()[1]
for _ in range(300):
    if rid in dt['delegate_result']() and 'completed' in dt['delegate_result'](): break
    time.sleep(.02)
assert rid in dt['delegate_result']()
```

`delegate_async`, `delegate_result` and `delegate_cancel` share one
[`Background`](https://vedicreader.github.io/ramabana/tools.html#background).
The run id comes back at once and the answer later. `delegate_result`
with no id lists every background delegation and its state.

``` python
bg_be = FakeBackend()
bgs = {t.__name__: t for t in subagent_tools(lambda: bg_be, lambda: ts)}

started = bgs['delegate_async']('survey the tree')
rid = started.split()[1]
assert started.startswith('started run_') and 'read-only' in started, started
assert 'arrives by itself' in started and 'Collect' not in started, started

for _ in range(300):
    if 'completed' in bgs['delegate_result']().splitlines()[0]: break
    time.sleep(.02)
test_eq(bgs['delegate_result'](rid).split('\nstate:')[0], 'sub answer')
test_eq(len(bg_be.spawned), 1)
assert rid in bgs['delegate_result']()
assert bgs['delegate_result']('run_nope').startswith('no delegation named')
assert bgs['delegate_cancel'](rid).startswith(f'{rid} had already finished')
```

`delegate_tell` sends a message to a sub-agent that is still running, in
the background or in the calling turn’s tree. The sub-agent reads it
attached to its next tool result, and an unknown or finished run is
refused with an error.

``` python
tell_bg = Background()
child = Run('run_kid', 'background', 'survey the tree')
tell_bg.runs[child.id] = child
tt = {t.__name__: t for t in subagent_tools(lambda: bg_be, lambda: ts, background=tell_bg)}
assert tt['delegate_tell']('run_kid', 'skip the tests') == 'told run_kid'
nxt = _inboxed(lambda: 'contents', lambda: child)()
assert '<user-message' in nxt and 'skip the tests' in nxt
assert 'no sub-agent named' in tt['delegate_tell']('run_nope', 'hi')
child.finish()
tt['delegate_tell']('run_kid', 'too late')
```

A sub-agent sees pictures only when they come with its first message.
`images` names them by path inside the open folders, and
[`read_pictures`](https://vedicreader.github.io/ramabana/tools.html#read_pictures)
reads them, refusing any path outside the folders or any file that is
not a picture. The question then ends with a note naming the pictures in
order, so the sub-agent knows which is which. A model that cannot take
images gets an error rather than a question without its pictures, and so
does a session with no way to read them. A `cloud_model` that names no
reachable model gets an error saying to leave it empty and use the
sub-agent model.

``` python
pic_root = Path(tempfile.mkdtemp()).resolve(); pic_host = LocalHost([pic_root], index=False)
png = b'\x89PNG\r\n\x1a\n' + bytes(32)
(pic_root/'refs').mkdir(); (pic_root/'refs'/'rama.png').write_bytes(png); (pic_root/'notes.txt').write_text('hi')
test_eq(read_pictures(pic_host, ['refs/rama.png']), ([png], ''))
test_eq(read_pictures(pic_host, []), ([], ''))
assert 'not a picture' in read_pictures(pic_host, ['notes.txt'])[1]
assert failed(read_pictures(pic_host, ['refs/missing.png'])[1]) and failed(read_pictures(pic_host, ['../x.png'])[1])

pic_be = FakeBackend()
pt = {t.__name__: t for t in subagent_tools(lambda: pic_be, lambda: [], None, lambda m: None, background=Background(),
                                            get_pictures=lambda ps: read_pictures(pic_host, ps))}
assert 'sub answer' in pt['delegate_search'](['match this face'], images=['refs/rama.png'])
sent = pic_be.spawned[-1].sent[0]
test_eq(sent[0], png); assert 'match this face' in sent[-1] and 'refs/rama.png' in sent[-1], sent[-1]
assert 'not a picture' in pt['delegate_search'](['q'], images=['notes.txt']); test_eq(len(pic_be.spawned), 1)
test_eq(pic_be.spawned[-1].sent[0], sent)
pt['delegate_search'](['plain']); test_eq(pic_be.spawned[-1].sent[0], 'plain')
rid = pt['delegate_async']('draw from it', images=['refs/rama.png']).split()[1]
for _ in range(300):
    if pt['delegate_result'](rid).startswith('sub answer'): break
    time.sleep(.02)
test_eq(pic_be.spawned[-1].sent[0][0], png)
blind = {t.__name__: t for t in subagent_tools(lambda: pic_be, lambda: [])}
assert failed(blind['delegate_search'](['q'], images=['refs/rama.png']))
missing = pt['delegate_search'](['q'], cloud_model='gpt-6-typo')
assert 'gpt-6-typo' in missing and 'leave `model` empty' in missing, missing
assert 'images' in get_schema(pt['delegate_search'])['input_schema']['properties']
```

`model` and `effort` name a model and an effort level for every
question, or give a list with one per question. A list of another length
is refused before any sub-agent runs. `get_sub(model, effort)` turns
each pair into a backend, the options `spawn` takes and a note for the
answer. It runs in the caller’s thread, before any sub-agent starts. A
pair named twice is resolved once. A question whose model cannot run
gets the error as its answer. The other questions still run.
`cloud_model` is still taken as `model` and is not in the schema.
[`delegate_many`](https://vedicreader.github.io/ramabana/tools.html#delegate_many)
runs the questions in
[`lanes`](https://vedicreader.github.io/ramabana/tools.html#lanes):
every write in one lane, the questions on a local engine in one, and
each cloud question in its own. Sub-agents on different cloud models
therefore answer at once.

[`picture_mime`](https://vedicreader.github.io/ramabana/tools.html#picture_mime)
recognises a PNG, JPEG, WebP or GIF by its first bytes, whatever the
file is called. A `fake.png` that holds text is therefore refused below.
The limits are the terminal’s: at most `MAX_ATTACH` (8) pictures, each
at most `MAX_MEDIA` (20 MB). Every question’s sub-agent receives every
picture. Pictures times questions may not exceed `MAX_ATTACH`.

A read-only sub-agent’s `generate_image` refuses a `path`, because
saving there would be a write. With `path` empty, it saves under
`media/`. `PATH_WRITES`,
[`path_write`](https://vedicreader.github.io/ramabana/tools.html#path_write)
and
[`write_targets`](https://vedicreader.github.io/ramabana/tools.html#write_targets)
tell the approval gate about this case. A call to a tool in
`PATH_WRITES` counts as a write when it names a `path`.
[`write_targets`](https://vedicreader.github.io/ramabana/tools.html#write_targets)
lists the files any call will write.

``` python
for name, body in (('Makefile', 'all:\n'), ('.env', 'K=1\n'), ('fake.png', 'text'), ('logo.svg', '<svg xmlns="x"/>')):
    (pic_root/name).write_text(body); assert 'not a picture' in read_pictures(pic_host, [name])[1], name
test_eq(read_pictures(pic_host, ['refs/rama.png'])[0], [png])
assert 'at most' in read_pictures(pic_host, ['refs/rama.png'] * (MAX_ATTACH + 1))[1]
with open(pic_root/'big.png', 'wb') as f: f.write(png); f.truncate(MAX_MEDIA + 1)
assert 'over' in read_pictures(pic_host, ['big.png'])[1]
too_many = pt['delegate_search'](['q%d' % i for i in range(MAX_ATTACH + 1)], images=['refs/rama.png'])
assert 'at most' in too_many, too_many
def generate_image(prompt: str, path: str = '') -> str: return f'drew {path or "media"}'
drawn = FakeBackend()
delegate(drawn, 'draw', tools=[generate_image])
sub_draw = {t.__name__: t for t in drawn.spawned[-1].tools}['generate_image']
assert failed(sub_draw('x', path='main.py')) and 'read-only' in sub_draw('x', path='main.py')
test_eq(sub_draw('x'), 'drew media')
delegate(drawn, 'draw', tools=[generate_image], writes=True)
test_eq({t.__name__: t for t in drawn.spawned[-1].tools}['generate_image']('x', path='a.png'), 'drew a.png')
```

[`delegate_many`](https://vedicreader.github.io/ramabana/tools.html#delegate_many)
registers every child run before any work starts. Cancelling the parent
stops the child that is running, and the children still queued never
spawn. The fake backend below blocks on its first question until it is
cancelled.

``` python
class _Sub:
    max_steps = 0
    def __init__(self, owner): self.owner, self.release = owner, threading.Event()
    def send(self, question, run=None):
        self.owner.started.append(question); self.release.wait(); return 'late'
    def cancel(self): self.owner.cancelled += 1; self.release.set(); return True
    def close(self): pass

class _ParentBackend:
    def __init__(self):
        self.spec = AttrDict(name='fake-child', local=False)
        self.started, self.spawned, self.cancelled = [], 0, 0
    def spawn(self, **kw): self.spawned += 1; return _Sub(self)

backend, parent, box = _ParentBackend(), Run('run_parent', grace=.03), []
parent.start()
t = threading.Thread(target=lambda: box.extend(delegate_many(backend, ['a', 'b', 'c'], n_workers=1, parent=parent)), daemon=True)
t.start()
while not backend.started: time.sleep(.001)
test_eq(len(parent.children), 3)
parent.cancel(); t.join(.2)
test_eq((backend.spawned, backend.cancelled), (1, 1))
test_eq(len(box), 3)
assert all((isinstance(x, dict) and x['state'] in ('cancelled', 'detached'))
           or (isinstance(x, str) and 'cancelled' in x) for x in box)
```

## Attachments

A prompt can carry files: pictures and sound. These helpers live here
and not in the terminal module. A host that only sends files then does
not load the terminal UI.
[`media_paths`](https://vedicreader.github.io/ramabana/tools.html#media_paths)
reads a paste or a dropped file in the shapes terminals deliver it,
quoted, backslash-escaped or as a `file://` URL, and returns the media
files it names, or an empty list for any other text.
[`attach_refs`](https://vedicreader.github.io/ramabana/tools.html#attach_refs)
finds `@path` references in a prompt.
[`clipboard_png`](https://vedicreader.github.io/ramabana/tools.html#clipboard_png)
reads a picture from the clipboard with `pngpaste`, `wl-paste` or
`xclip`, whichever is installed.
[`Attachment`](https://vedicreader.github.io/ramabana/tools.html#attachment)
holds one file’s bytes, read once.
[`media_parts`](https://vedicreader.github.io/ramabana/tools.html#media_parts)
and
[`media_note`](https://vedicreader.github.io/ramabana/tools.html#media_note)
turn a list of attachments into what the message sends: the bytes the
model accepts, and a note listing every file. Audio goes to the model
only when it accepts audio; otherwise the note gives the file’s path.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/tools.py#L988"
target="_blank" style="float:right; font-size:smaller">source</a>

### media_note

``` python
def media_note(
    atts, spec:NoneType=None
):
```

*What the message says about the files attached to it.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/tools.py#L983"
target="_blank" style="float:right; font-size:smaller">source</a>

### media_parts

``` python
def media_parts(
    atts, spec:NoneType=None
):
```

*The attachments that go out as model content parts.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/tools.py#L977"
target="_blank" style="float:right; font-size:smaller">source</a>

### sendable

``` python
def sendable(
    atts, spec:NoneType=None
):
```

*The attachment kinds `spec`’s model accepts: images always, and audio
when it takes audio input or no spec is given.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/tools.py#L963"
target="_blank" style="float:right; font-size:smaller">source</a>

### Attachment

``` python
def Attachment(
    path
):
```

*One media file for the next prompt, its bytes read once when attached.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/tools.py#L954"
target="_blank" style="float:right; font-size:smaller">source</a>

### clipboard_png

``` python
def clipboard_png():
```

*The clipboard picture as PNG bytes, or None.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/tools.py#L938"
target="_blank" style="float:right; font-size:smaller">source</a>

### attach_refs

``` python
def attach_refs(
    text
):
```

*Media named `@path` or `@"path"` in a prompt, trailing punctuation
stripped from a bare one.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/tools.py#L934"
target="_blank" style="float:right; font-size:smaller">source</a>

### ref_tokens

``` python
def ref_tokens(
    text
):
```

*Each `@path` in `text` as `(path, quoted)`; a bare one keeps whatever
punctuation follows it.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/tools.py#L919"
target="_blank" style="float:right; font-size:smaller">source</a>

### media_paths

``` python
def media_paths(
    text
):
```

*Every media file a paste names, or `[]` when it is anything else.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/tools.py#L913"
target="_blank" style="float:right; font-size:smaller">source</a>

### is_media

``` python
def is_media(
    p
):
```

*Whether `p` names a media file that exists.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/tools.py#L903"
target="_blank" style="float:right; font-size:smaller">source</a>

### media_path

``` python
def media_path(
    s
):
```

*A `Path` from one path as a terminal delivered it, quoted, escaped or
as a `file://` URL, or None.*
