(^、^7 gatto — an AI assistant that runs on your own PC

Write an extension

An extension is a C# script file that gatto compiles when it starts. It gives gatto a new tool — something it can decide to call, the same way it decides to read a file or run a command. There is no SDK, no build step and no manifest to fill in.


Where extensions live

Only in your home folder:

~\.gatto\extensions\my_tool.csx
~\.gatto\extensions\my_tool\main.csx     <-- a folder works too, one level deep

gatto never loads an extension from the project you have open. That is the whole security posture in one sentence: opening someone else's repository cannot hand gatto a new tool, because gatto is not looking there. What it can do is decided by what is in your folder, before you open anything.

Extensions are discovered at launch. Add a file, restart gatto, and the tool is there.


The shape of a tool

One call registers one tool. The host object is bound as Gatto:

Gatto.Register(
    "tool_name",
    "What the model reads when it decides whether to call this.",
    """{"type":"object","properties":{"query":{"type":"string"}},"required":["query"]}""",
    async (args, ctx, ct) => new ToolResult("the text the model sees", Gloss: "one short line"));

Four arguments, and each has a job:

  • the name — lowercase, with underscores. This is what the model calls.
  • the description — written for the model, not for a human reading docs. It is the only thing the model has when it decides whether this tool is the right one. Say what it does and when to reach for it.
  • the schema — a JSON Schema object describing the arguments, as a string. C# raw string literals ("""…""") let you paste JSON without escaping every quote. It is parsed once, at registration, so a malformed schema fails at launch rather than on the first call.
  • the body — an async delegate taking the parsed arguments, a context, and a cancellation token. Return a ToolResult.

What comes back

new ToolResult(string Text, bool IsError = false, string? Gloss = null)

Text is what the model reads. IsError marks a failure the model should react to rather than treat as data. Gloss is the one short line a person sees in the transcript — keep it to a few words, because it is a status line, not the result.

Validation failures should throw. gatto catches the exception and hands the model the message as an error result, so throw new Exception("query must not be empty") does the right thing and you never build the error envelope yourself.


A worked example

A small tool that fetches a URL and hands the model the text of the page. It is a simplified cousin of the web_fetch that ships with gatto.

// page_text.csx -- fetch a URL and return its text.
using System.Net.Http;
using System.Text.RegularExpressions;
using Gatto.Core.Tools;

var http = new HttpClient();
// Always set a Timeout explicitly. The default is 100 seconds and it will not be
// what you meant; if you want the caller's token to be the only deadline, say so.
http.Timeout = Timeout.InfiniteTimeSpan;

Gatto.Register(
    "page_text",
    "Fetch a web page and return its visible text. Use when the user gives you a URL.",
    """{"type":"object","properties":{"url":{"type":"string"}},"required":["url"]}""",
    async (args, ctx, ct) =>
    {
        var url = args.GetProperty("url").GetString() ?? "";
        if (!url.StartsWith("http://") && !url.StartsWith("https://"))
            throw new Exception("url must start with http:// or https://");

        using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
        cts.CancelAfter(TimeSpan.FromSeconds(30));
        var html = await http.GetStringAsync(url, cts.Token);

        var text = Regex.Replace(html, "<script.*?</script>|<style.*?</style>", " ",
                                 RegexOptions.Singleline | RegexOptions.IgnoreCase);
        text = Regex.Replace(text, "<[^>]+>", " ");
        text = System.Net.WebUtility.HtmlDecode(text);
        text = Regex.Replace(text, @"\s+", " ").Trim();
        if (text.Length > 20000) text = text.Substring(0, 20000) + " ... [truncated]";

        return new ToolResult(text, Gloss: $"{text.Length} chars from {new Uri(url).Host}");
    });

Save that as ~\.gatto\extensions\page_text.csx, restart gatto, and ask it what is on a page.

Two things the example is showing you on purpose

  • The timeout is explicit. An HttpClient with a default timeout will cut your call off at 100 seconds no matter what deadline the caller thought it had. Set it, then own the real deadline with a linked token.
  • Fetched text is not trusted. Whatever comes back is someone else's writing, and the model will read it as if you handed it over. Strip it, bound it, and do not let it in unbounded.

Hooks

Besides tools, an extension can watch what the session does:

Gatto.On("tool_call", async payload => { /* runs before a tool executes */ });

Four events: tool_call, tool_result, message_end, session_summary. One of them is different in a way worth knowing: a tool_call handler that throws blocks the tool — that is how a hook says no. The other three fail open, so a broken handler cannot take the session down with it. tool_result handlers are observe-only.

Gatto.Log("...") writes to gatto's extension log, which is where to put anything you want to read afterwards.


Testing it

  • A compile error is loud. If the script does not compile, gatto reports the diagnostics at launch and carries on without it — it never half-loads.
  • Ask gatto to call it. The fastest check is a real session: start gatto and ask for the thing your tool does. If the model never reaches for it, the description is the problem, not the code.
  • Check the gloss in the transcript. If the one-line summary does not tell you what happened, it will not tell you when something has gone wrong either.
  • Write it with gatto. Ask gatto to write an extension for itself, then read what it produced before you drop it in.

Getting it onto this site

The extensions listed on the extensions page were each read line by line by a person before they went up. If you have written something you think belongs there, open a pull request against the gatto-extensions repository and say what it does and why it needs the access it asks for.

Nothing stops you installing your own without asking anyone — it is your folder. The review is what the listing on this site means, not what running an extension requires.

GGitHub XX BBluesky MMastodon RReddit Windows 10 / 11 GPL-3.0
  /l、
(^、^7   zzz
  l  ~ヽ
  じしf_,)ノ

nothing here yet — a game goes in this space

Close gatto.computer
(^、^7

Are you sure you want to close this window?

It's now safe to close
this tab.
…or come back.