Browse documentation

Packages

Add registry packages to a project, pin an immutable snapshot, and override a dependency with a local path or a git fork.

A Katari project declares its dependencies in katari.toml. Each name in [dependencies].packages resolves through a pinned registry snapshot — a curated set of (package, version) pins that are guaranteed to compile together — or through a local [overrides] entry. The CLI fetches each pinned tarball, verifies its content hash, and records the exact resolution in katari.lock.

Add a package

katari add tavily

katari add checks that the name is resolvable (present in the pinned snapshot, or covered by a root [overrides] entry), rewrites the packages array in katari.toml, fetches and verifies the pin, and refreshes katari.lock. katari remove is the same edit in the other direction.

After adding, import the package by its name and call its agents qualified:

import tavily
 
@"Search the web; failures degrade to a readable line instead of failing the run."
agent main(question: string) -> string with io {
  use handler {
    request prelude.throw(error: env.missing_secret | http.status_error | http.fetch_error | json.parse_error) -> never {
      break f"search failed: ${json.stringify(value = error)}"
    }
  }
  use tavily.provider(api_key = env.get_secret(key = "TAVILY_API_KEY"))
  tavily.search(query = question)
}

The env.get_secret call reads the API key from the project's env store — see Secrets and credentials.

Pin a snapshot

The [dependencies] section names the registry and the snapshot every package resolves against:

[dependencies]
registry = "https://raw.githubusercontent.com/katari-lang/katari-registry/main"
snapshot = "snapshot-2026-07-17-c47a6e67"
packages = ["tavily"]
  • A snapshot is a consistent set: every package in it compiles against every other, with the katari_compiler version the snapshot declares. Once cut, a snapshot file is immutable — pin one and your resolution never shifts under you. This is the recommended setting.
  • snapshot = "staging" points at the registry's mutable candidate set — the packages that will become the next snapshot. Use it for early access to a just-merged package, and switch back to the next immutable cut.

There are no per-package version constraints to solve: the snapshot already fixed every version, so packages is a flat list of names.

Commit the lockfile

katari add / katari remove (and a resolve during katari apply) write katari.lock next to katari.toml:

[lock]
version = 1
snapshot = "snapshot-2026-07-17-c47a6e67"
 
[packages.tavily]
source = "git"
url = "https://github.com/katari-lang/katari-package-tavily"
rev = "2590e15f6ef3852af3d361b019633340501911fe"
sha256 = "6d59a5fb4d86ef59e73c80531503b512f8f4f977a7a97c216451dec515de286b"

Commit it: every non-path dependency is pinned to a git rev plus a verified tarball hash, so every consumer of the repository gets the same byte-for-byte resolution. A path override is recorded without a hash — a local path is mutable on purpose.

Override a dependency

An [overrides.<name>] table replaces where one declared dependency comes from, without touching the snapshot pin for everything else:

[dependencies]
registry = "https://raw.githubusercontent.com/katari-lang/katari-registry/main"
snapshot = "snapshot-2026-07-17-c47a6e67"
packages = ["tavily", "discord"]
 
# Develop a package against the app that uses it:
[overrides.tavily]
path = "../katari-package-tavily"
 
# Or pin a fork; the rev must be a full 40-character commit SHA:
[overrides.discord]
git = "https://github.com/you/katari-package-discord"
rev = "b93423583ba35d85aca3f0de80823b24cb77f9f9"

Each override sets path or git (with git requiring rev), and must name a dependency declared in packages — a typo'd override is an error rather than a silently ignored table. A git override's rev is deliberately a commit SHA, not a branch or tag: with no separate content hash, the rev is the only thing pinning reproducibility.

What is in the registry

The first snapshot (snapshot-2026-07-17-c47a6e67) carries eight packages. Every exported agent, request, and type is documented in the reference.

  • ai — a provider-agnostic AI tool-calling loop: one infer_step seam with interchangeable Anthropic, Gemini, and OpenAI providers.
  • discord — a discord.js gateway client: a connection provider, watch / send agents, and file attachments in both directions (ships an FFI sidecar).
  • e2b — run Python in a persistent e2b sandbox as a tool (ships an FFI sidecar).
  • google_calendar — calendar tools (list / create / watch) over the OAuth refresh-token grant — pure Katari, no sidecar.
  • imagegen — edit images with the Gemini image model as a tool (ships an FFI sidecar).
  • slack — a Slack bot capability over Socket Mode: watch channel messages, post replies (ships an FFI sidecar).
  • tavily — web search as a tool over the Tavily API — pure Katari, no sidecar.
  • web — fetch a web page as a tool over http.fetch — pure Katari, no key.

Where to go next

  • The tutorial's Discord bot composes seven of these packages into one app.
  • FFI sidecars — how a package like e2b or discord ships TypeScript alongside its Katari source.
  • CLIkatari add, katari remove, and the rest of the toolchain.