Reference for your agent. The precise shape of things — signatures, fields,
rules — written so an agent can read it and act. You do not need to: ask your
agent for what you want in words, and if it needs this page, hand it the URL or
use Copy page. What to ask for, and how to check it, is in Ask;
how to see what changed is in Look inside.
tools/, and it runs inside the
sandbox of whoever is talking to the harness rather than on the platform.
tools=["tools/notes.py"] and it has a tool called notes__write,
described by that first docstring line, taking two required strings. Nothing else
is registered, deployed or restarted: the file is the tool.
Why a function and not a JSON call
Most of what an agent does on Splox is written as code, not as a tool call. The agent’s own tool iscompute_exec, which executes Python inside the sandbox, and
every other tool of the platform is a Python function sitting on that same
filesystem, ready to import:
if, they would
still be one round trip. The same work as separate JSON tool calls is four
messages through the model, each carrying the last one’s output back through the
context window.
That is the opinion the platform has, and it is worth understanding before you
fight it: the model writes a program, and the program calls the tools.
Branching, retrying, filtering and aggregating happen in Python, where they cost
nothing, instead of in the conversation, where each step costs a turn.
The projected packages live under ~/tools/, one directory per service, one file
per tool:
Your own tools
Your harness’stools/ directory is different in shape and in purpose. It is
flat, one file per group, and its tools are offered to the model directly, by
name.
tools/*.py counts. Subdirectories are not scanned, and a file whose name
starts with an underscore is a helper rather than a tool — the loader skips it,
and the other files import it, because the tools directory and its parent are
both on sys.path.
A tool’s name is the file plus the entry point joined by two underscores. The
separator is not a dot because a provider refuses the whole request when one tool
name fails ^[a-zA-Z0-9_-]{1,128}$, and a dotted name fails it. Name the file the
way you would name a Python module — letters, digits and underscores only — since
a source id like tools/my-tool.py is not one the platform can resolve.
A harness tool is usually not new capability. It is capability with a smaller
surface and a docstring that says what to do with it:
compute_read_file the agent would call directly, wrapped in
something narrower and better described.
What the model sees
Three things, and only three: the name, the first line of the docstring, and the schema derived from the type hints. The docstring is not documentation. The first line of the docstring is the description; theArgs: block below it describes the parameters; everything else
in that docstring is for the human reading the file, and the model never sees it.
notes.py at the top of this
page:
The type hints are the schema
The mapping is fixed and never guessed:
Anything else — an unsupported annotation, a missing hint,
*args, **kwargs, a
positional-only parameter — is an error against the file, and that error is what
refuses a publish. Positional-only is refused for the plainest of reasons: the
server calls the tool with keyword arguments.
A class, when calls should share state
A file holds either one public class or plain module-level functions. If there is a public class, its public methods are the tools and the module-level functions are not. Two public classes in one file is an error against that file, and nothing in it is exported.scope decides how long the instance lives. The default "call"
builds a fresh one for every call; "run" builds one and keeps it for the whole
run, which is the difference between a shell that forgets the directory you were
in and one that does not. Functions have no instance and so have no scope. The
model is shown the name, the description and the schema, and never this.
The class docstring is appended in parentheses to every one of its methods’
descriptions, which is how a whole file says one thing once:
__init__ takes an argument is handed a ctx:
ctx.tool("shell")is the instance of another file of the same tree — the very instance the run is already using, not a second one, which is how an editing tool resolves its relative paths against the directory a shell tool is sitting in. A file cannot ask for itself that way.ctx.system.<service>is the platform’s own tool package, imported on first touch:ctx.system.search.search(...)is the same import asfrom tools.search import search.
@tool from splox.toolkit narrows the file to the
marked ones. With no @tool anywhere, every public function is a tool. Order is
source order, not alphabetical.
What comes back, and what happens when it does not
Whatever the tool returns has to be JSON, and a result that is not is an error before it reaches the wire. A string longer than 20000 characters is written whole to/tmp/tool-<uuid>.txt
in the sandbox and cut to its first 4000 and last 1000 characters. The model is
told the path along with the cut result, and can read the rest with the file tools
it already has.
An exception is not a crash. It comes back as <Type>: <message>, the model reads
it as a failed tool call, and the run continues — so a ValueError whose message
says what to do instead is the cheapest teaching a tool can do:
TimeoutError, the server stays up, and the next call is served normally.
Editing one
On every list and every call, the server compares the mtime of each*.py in the
tree. A file that moved is re-imported and its kept instance dropped, and the
others are left alone. Edit a tool in the checkout and the next call runs the new
code — no restart, and no publish either.
Two consequences worth knowing before you go looking for them:
- The catalog is asked once per run, so a signature or a description changed mid-run reaches the model on the next run, while the body of the tool is re-read on the very next call.
- The reload is per file: editing
_helpers.pydoes not re-import the tool file that imported it, so touch that file as well.
Dependencies
Three files, at the root of the tree and nowhere else:requirements.txt,
pyproject.toml and setup.sh. They are files rather than fields of a
declaration because what reads them is pip, and pip already has a format for
each. Only the [project] dependencies of the pyproject.toml are read, and
setup.sh is for the things pip cannot say: an apt package, a model download, a
directory to create.
They are applied in that order — declared packages first, the script last —
because a setup script that needs a package is ordinary and a package that needs
the script is not.
The install runs in the sandbox as part of the same step that checks the tree out,
before anything starts the tool server, and it is stamped by the sha256 of those
files’ names and bytes: a turn that changed nothing runs no pip at all, and one
added line means a fresh install. pip install --user, a 240-second budget shared
by all three steps, and the whole transcript in /tmp/splox-harness-deps.log —
which is the file to read when a tool comes back with an ImportError for
something the tree clearly declares. A failed install does not fail the checkout:
the tree is there either way.
The three things a tools entry names
An agent gets a tool by naming its source in thetools list of its declaration.
Each entry is one string, and it is one of three things, told apart by shape:
tools/, flat,
ending in .py, no absolute path and no ... Naming a file the harness does not
carry gives the agent a source with nothing behind it, so the name is worth
reading twice. Naming one file gives the agent that file’s tools and only those: a
call to a tool from another file is refused rather than run, even though one
server holds the whole tree.
A source alone means every tool it exposes. An entry may narrow that to named
tools by writing them after a #:
hooks/tools.py, which sees the call by
name with its arguments before it runs.
A tool used by two agents is written out in both declarations, and each agent’s
list is part of what that agent is. There is no shared declaration to keep in
sync.
To find a tool server id, or the slugs to narrow it to, ask the platform:
harness_mcp_servers lists the servers this account has connected and
harness_mcp_tools lists what each exposes. No file in the checkout has them, and
a push naming one wrongly is refused.A platform source is not one of your files
system:compute, system:search, system:media and the rest are services the
platform runs and owns. Their tools exist whether or not your harness does, their
catalog comes from the platform, and they are dispatched, billed and credentialed
by it. You cannot edit one, and you do not have to publish anything for one to
work.
system:compute is the odd member of the set: it is the runner every other tool
executes through, which is why your own tools import it rather than the agent
calling it for them.
A file of tools/ is the opposite in every respect. It is code in your
repository, it has no row anywhere, its catalog is a question asked of a Python
process in a sandbox that belongs to whoever is talking to the harness, and it can
be broken — a file that does not import refuses the publish with the interpreter’s
own words rather than a sentence of the platform’s, and only the first error,
because one broken module reports itself once per file that touched it.
That is the trade. A platform source is capability you are handed; a file in
tools/ is capability you write, in the smallest and best-described shape the job
needs, and it is yours to keep working.
