> ## Documentation Index
> Fetch the complete documentation index at: https://docs.splox.io/llms.txt
> Use this file to discover all available pages before exploring further.

# A tool of your own

> Ask your agent for an ability it does not have, and watch it write the tool into its own harness, test it, and publish it

Your agent can do a great many things out of the box, and none of them are
*remembering the call you had with Anna*. A [tool](/reference/tools) is how an
ability that is specific to you gets added — a Python function in your agent's
own [harness](/concepts/harness), offered to the model by name.

You do not write the function. You describe the ability, and the agent writes it,
tries it, fixes what it got wrong, and publishes a new version of itself.

## What you say

One message, in an ordinary chat:

```text theme={null}
I want to be able to tell you "log a call with Anna about the renewal" and have
you keep it, so that later — in a new chat, next week — I can ask "what were the
last calls with Anna" and get them back. Give yourself a tool for that, keep the
data on this machine, and publish it when it works. Tell me which version that
made.
```

Read that again for what is *not* in it. No file names, no function signatures,
no mention of JSON or CSV. What it does contain is the four things that decide
whether the tool comes out right:

|                                |                                                                                   |
| ------------------------------ | --------------------------------------------------------------------------------- |
| **What you will say to it**    | "log a call with Anna about the renewal" — the phrasing the tool has to recognise |
| **What you will ask it later** | "the last calls with Anna" — which is what makes it a *log* rather than a note    |
| **Where the data lives**       | on this machine, so it outlives the chat                                          |
| **That it gets published**     | otherwise it is a file in a sandbox, not an ability                               |

## What came back

The agent read the tools its harness already had, took `tools/notes.py` as the
house pattern for "keep data on this machine", and wrote a new file with three
functions: **`calls__log`**, **`calls__recent`**, **`calls__forget`**. The store
is one append-only JSONL file at `~/.calls/calls.jsonl`. It added the file to the
Assistant's tool list, added one line to the Assistant's prompt so the model
reaches for it unprompted, wrote a test case into the harness, committed, and
pushed.

Three functions, not one, and the third is `forget` — because an append-only log
that cannot be corrected is a log people stop trusting. Nothing in the message
asked for that. This is the part you get for describing an ability instead of a
function.

<Accordion title="The tool it wrote, as the model sees it">
  A tool file is plain Python. The platform derives the tool's description from the
  first line of the docstring and its arguments from the type hints, so the
  docstring is not a comment — it is the contract the model reads before calling.

  ```python theme={null}
  def log(contact: str, about: str, when: str = "", notes: str = "") -> str:
      """Log a phone call with a client: who, what it was about, when — appended to the call log this machine keeps, so it is still there in a new chat next week.

      The log is append-only: logging the same person twice is two calls, which
      is what you want. Say who it was with the way you say it everywhere else
      ("Anna", "Anna Smith") — recent() matches on part of the name, so one
      consistent spelling per person keeps their history in one place.

      Args:
          contact: Who the call was with — a person or company, e.g. "Anna".
          about: What the call was about, in one line, e.g. "renewal terms for Q4".
          when: When the call happened: an ISO date like 2026-09-01, or with the
              time, like 2026-09-01T15:04. Empty means the call just happened.
          notes: Whatever is worth keeping from the call — decisions, numbers, next steps.
      """
      contact = contact.strip()
      about = about.strip()
      if not contact:
          raise ValueError("contact is who the call was with — e.g. contact='Anna Smith'")
      if not about:
          raise ValueError(
              "about is what the call was about, in one line — e.g. about='renewal terms for Q4'"
          )
  ```

  Those `ValueError`s are not defensive programming. A tool that raises goes back
  to the model as text, so the message is an instruction to whoever misused it —
  and whoever misused it is the model, which will read it and call again correctly.
  The same file refuses an unparseable date rather than silently logging today:

  > `when must be an ISO date, like 2026-09-01 or 2026-09-01T15:04 — %r is not one.`
  > `Leave when empty to log the call as happening now.`
</Accordion>

<Accordion title="Wiring it in — the one line that makes it exist">
  A file in `tools/` is nothing until an agent is given it. That is one line in
  `programs/splox/main.py`:

  ```python theme={null}
  tools=["system:compute", "tools/files.py", "tools/notes.py", "tools/calls.py"],
  ```

  The agent edited that line itself. See [Tools](/reference/tools) for the naming rule
  that turns `log` in `calls.py` into `calls__log` at the model.
</Accordion>

## It tested itself, and found its own bug

Before pushing, the agent ran the platform's own schema deriver over the file —
the same code the tool server runs — and confirmed what the model would be shown:
`contact` and `about` required, `when` and `notes` optional with defaults. Then
it exercised every path against a throwaway home directory.

Two of its own test assertions turned out to be wrong rather than the tool, which
it worked out and said so. One real bug it did find and fix: `forget()` reported
the deleted call as `00:00: kickoff`, which reads like nonsense. It rewrote the
message.

It also left a test behind in the harness, and the second scenario in it is the
whole point of the tool:

> The call log: one conversation, two turns — log a call, then read it back in a
> second run. The second scenario is the one that matters: it passes only if what
> the first run logged is still on this machine's disk when the next run asks for
> it.

<Note>
  Evals run against the **published** version, not your working copy. The agent
  discovered this by trying it the other way round: publish first, then run the
  case against the new version. See [Evals](/reference/evals).
</Note>

## Publishing, on a machine somebody else is also using

The push is the publish. It goes through the platform's git proxy, which loads
the tree and refuses it if a file does not import — so a broken tool cannot
become a version.

This particular push was refused, and the reason is worth reading:

<Frame caption="The push is refused, and the agent works out why: a second agent is committing to the same harness">
  <img src="https://mintcdn.com/sploxltd-165e0515/FtagtnY5r9E1DKmP/images/tutorials/custom-tool-publish.png?fit=max&auto=format&n=FtagtnY5r9E1DKmP&q=85&s=649a7c5543d8f2832bc4e7fc66335bf9" alt="A Splox chat where the agent diagnoses a refused push caused by a concurrent agent's commit" width="2560" height="1600" data-path="images/tutorials/custom-tool-publish.png" />
</Frame>

Two rules surfaced at once. The tree takes **programs, tools, evals and root
docs** — a root `.gitignore` is refused, and one had been added by somebody else's
commit. And the agent's own commit had already reached the remote, carried there
by that other agent's successful push a minute earlier.

The version history bears that out. The tool went up inside a version whose
message is about something else entirely:

```text theme={null}
 9  c2d05bd  Keep pycache out of git with a program-local .gitignore
10  380bbb3  telegram: answer messages with the harness's Assistant, one Splox conversation per chat
11  e5a7189  telegram: drop a compiled file committed by mistake, ignore __pycache__ within the program
```

<Warning>
  A harness is a git repository, and a machine you share is a repository you share.
  If two agents work on the same harness at once, one of them is rebasing onto the
  other. It resolves the way git always resolves, and nothing is lost — but do not
  expect the version number you were promised to be the version number you get, or
  its message to describe your change.
</Warning>

<Note>
  There is no version history screen in the app. Ask your agent — it reads the
  list from the platform and tells you which number carried what. See
  [Versions](/inside/versions).
</Note>

## What you have now

A published version of your agent with an ability nobody else's agent has. From
the next run onwards the model is offered `calls__log`, `calls__recent` and
`calls__forget` by name, alongside its built-in tools, and the data sits on the
machine rather than in a conversation — which is what makes the answer to "what
were the last calls with Anna" survive the chat you asked it in.

That last claim is the one to check yourself, and checking it is one message in a
**new** chat:

```text theme={null}
What were the last calls with Anna?
```

If the log is doing its job, the answer comes back from the file rather than from
the conversation — because the new chat has no conversation to remember.

<Note>
  The **Tools** screen in the app lists MCP servers and system tools. A tool that
  lives in your harness does not appear there — it is part of your agent, not a
  connection. Ask your agent what tools it has and it will read its own
  `main.py` and tell you.
</Note>

## Next

<CardGroup cols={2}>
  <Card title="A job that runs every day" icon="clock" href="/tutorials/nightly-job">
    The same publish-a-version loop, applied to something that runs without you.
  </Card>

  <Card title="Tools" icon="wrench" href="/reference/tools">
    The reference: how a file becomes a tool, what the docstring must carry, what an error does.
  </Card>
</CardGroup>
