Self-hosted service
easylink-ai-open/tiri-agent avatar
easylink-ai-open/tiri-agent

Tiri Agent: a local-first agent shell built on agent-runtime

Tiri local agent backend and web shell built on agent-runtime.

721 stars124 forksTypeScriptAGPL-3.0

At a glance

What is it?
Tiri Agent wraps the agent-runtime core in a CLI, a FastAPI server, a Next.js web shell, sandbox tools and chat channel adapters. It is for engineers who want to run a personal agent on their own machine, and it asks you to install a pinned sibling package first.
Who is it for?
Adopt Tiri Agent if you want a self-hosted agent with a real web shell and chat channels and you are comfortable installing a pinned sibling package from a local path. Do not adopt it if you need a published PyPI install, a stable release line, or a permissive licence for closed distribution, because the project is at version 0.1.0 and ships under AGPL-3.0-only.
Can I use it commercially?
Yes, with strict conditions. AGPL-3.0 is a network copyleft licence: if people use a modified version over a network, for example as a hosted service, you must offer them its source code under the same licence.
Is it still maintained?
Yes. The repository last received commits 77 days ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on September 17, 2026, and from our analysis. They are not legal advice.

DEEP OPEN-SOURCE ANALYSIS

What Tiri Agent is for, and who it is not for

Tiri Agent is a product shell, not an agent framework. The README describes it as a local-first agent product that packages the standalone agent-runtime core into a personal-agent experience: CLI chat, FastAPI APIs, a Next.js web shell, sandbox tools, channel integrations, local skills and SQLite persistence. The division of labour is explicit in the highlights: product policy lives in this repository, neutral agent mechanics stay in the sibling core package.

That split tells you who the project is aimed at. If you want to run an assistant on your own machine, point it at a model endpoint, give it a workspace it can read and write, and reach it from Telegram, Weixin or Feishu, Tiri is the assembled product. If you want a library to embed agent loops into your own service, the interesting code is one level down in agent-runtime, and Tiri is mostly the opinionated wrapper around it.

The repository is TypeScript according to its metadata, but the backend, packaging and install instructions are Python. The web directory is the Next.js and React part. Treat it as a Python backend with a JavaScript front end, not as a Node project.

How the runtime, prompts, tools and channels fit together

The repository map is the clearest description of the architecture. The agent package holds TiriBackend, backend config, logging hooks and collaboration modes. The prompts package assembles the system prompt from four sources: the base identity and operating loop in prompts/base.py, the runtime environment (cli, web, or channel:<provider>), an optional profile file, and an optional skill registry. The tools package composes the tool registry. The sandbox package exposes local and Docker sandbox implementations as runtime tools. The channels package holds provider adapters, listeners, a gateway, targets, settings and an outbox worker. The server package contains the FastAPI app, SQLite storage, event queues and the local server runner. The cli package is described as a thin tiri entrypoint.

Two design choices stand out. First, the same TiriBackend configuration path serves both the CLI and the web entry points, so a model setting or data directory does not have to be duplicated per surface. Second, the prompt is assembled per environment rather than fixed, which is why the environment string appears in the prompt sources: a channel-driven session can be told it is running as channel:telegram rather than as cli.

Top-level backend.py, config.py and collaboration.py are compatibility re-export modules. The README says new implementation should live under the structured packages, which is a polite way of saying the flat modules are legacy surface kept for import compatibility.

Installing Tiri Agent and running a first session

The README's quick start assumes the sibling runtime is already checked out next to this repository, because it installs agent-runtime from a relative path. Create the virtual environment and install both packages, plus pytest, in one go:

bash
uv venv --python 3.11 .venv
uv pip install -e ../agent-runtime -e ".[server]" --python .venv/bin/python
uv pip install pytest --python .venv/bin/python

The [server] extra is what pulls in FastAPI and uvicorn, so it is required if you intend to start the HTTP server. If ../agent-runtime does not exist on your disk, this command fails, and the README does not describe an alternative install route.

Next, point Tiri at a model. Four environment variables cover the common case:

bash
export TIRI_LLM_API=openai-chat-completions
export TIRI_MODEL=your-model-name
export TIRI_LLM_API_KEY=your-api-key
export TIRI_LLM_BASE_URL=https://api.openai.com/v1

TIRI_MODEL is required unless you pass --model on the command line, and TIRI_LLM_BASE_URL is optional for providers that do not need an override. With that set, the CLI entrypoint is a single command, and you should land in an interactive chat session:

bash
.venv/bin/tiri

To get the full product, start the server. The README states that this brings up FastAPI at http://127.0.0.1:8000 and Next.js at http://127.0.0.1:3000:

bash
.venv/bin/tiri server start

If you only want the API, add --no-web to that command. CLI flags mirror the common settings, so --model, --data-dir and, on the server command, --db-path are available when you would rather not export variables.

Sandbox tools: local workspace versus one Docker container per session

The sandbox is where Tiri stops being a chat wrapper. Two backends are documented. The local backend works against a mounted workspace root:

bash
export TIRI_SANDBOX_ROOT=/path/to/workspace
export TIRI_SANDBOX_BACKEND=local

The Docker backend keeps the same mounted workspace but executes shell commands inside a container:

bash
export TIRI_SANDBOX_BACKEND=local-docker
export TIRI_SANDBOX_DOCKER_IMAGE=python:3.12-slim

The README gives one detail that matters for reasoning about state: the Docker backend keeps one container per Tiri session, and the container name is derived from the session id, the mounted workspace and the image. That means a resumed session reuses its container, and later run_shell calls go through docker exec rather than starting fresh. Reuse is convenient for installed packages and working directories, but it also means container state accumulates across a session and is keyed to that combination of session, workspace and image. Change any of the three and you get a different container.

The tool set itself is small: read_file, write_file, list_files and run_shell. There is no documented patch tool, no diff review step, and no described approval gate before run_shell executes. If you enable the sandbox against a directory that holds anything you care about, the local backend gives the model the same file access your user account has. The Docker backend is the more contained option, and the README presents it as such.

Channels, the inbound endpoint and the token you must set

Three providers are supported: Telegram long polling via the Bot API, Weixin polling via the ilink bot API, and Feishu/Lark websocket via lark-oapi. Telegram and Weixin poll outbound; Feishu uses a websocket. The README also mentions proactive outbound messaging, which is why the channels package contains an outbox worker and proactive targets stored in the database.

Inbound channel events are routed through a single HTTP endpoint, POST /api/channels/inbound. The README is direct about the security boundary: when the server is exposed beyond localhost, set an inbound token and send it as either Authorization: Bearer ... or X-Tiri-Channel-Token.

bash
export TIRI_CHANNEL_INBOUND_TOKEN=local-secret

Background channel services are enabled by default, which is a detail worth noticing before your first run. Disable them explicitly if you do not want listeners and the outbox worker running:

bash
export TIRI_CHANNEL_GATEWAY_ENABLED=false
export TIRI_CHANNEL_OUTBOX_ENABLED=false

Nothing in the README describes rate limiting, retry policy or delivery guarantees for the outbox. If you plan to route real traffic through a channel, that is undocumented ground you would have to read the source to understand.

Persistence, collaboration mode and the upgrade surface

Tiri stores its durable state in SQLite, by default at ~/.tiri/tiri.db. According to the README, that database holds chat sessions, message history, event-stream records, local settings, channel settings, proactive targets and channel outbox entries. TIRI_DATA_DIR changes the data directory, and TIRI_DB_PATH or the server's --db-path flag points at an explicit database file. Because sessions, channel configuration and outbox records share one file, backing up or migrating an installation is a matter of moving that database, not a set of per-service stores.

TIRI_COLLABORATION_MODE accepts default or plan. The README lists those two values and does not explain what plan mode changes about the loop, so treat the mode as a documented switch with undocumented semantics until you read the collaboration module.

On upgrades, the honest summary is that the material is thin. The project is at version 0.1.0, the README retrieved here lists no releases, and the dependency on agent-runtime is pinned exactly at 0.2.5 rather than given a range. That pin means a Tiri upgrade and a runtime upgrade are separate decisions you have to sequence yourself. There is no documented migration path for the SQLite schema, and no documented rollback procedure. The last push to the repository was on 2026-07-02, so the code has been quiet for roughly two and a half months; the README does not describe a release cadence or a support window.

Licensing is straightforward to state and worth stating plainly. Tiri Agent is licensed under the GNU Affero General Public License, version 3 only, and the package metadata points at the LICENSE file. AGPL-3.0-only is a strong copyleft licence with a network-use clause. If you modify Tiri and let users interact with it over a network, the licence's terms are likely to reach your modifications. Whether that fits your distribution model is a question for your own legal review, not something this article can settle.

Where Tiri Agent is the wrong choice

Three cases argue against it. The first is installation. The documented quick start installs agent-runtime from ../agent-runtime, a sibling checkout on the local filesystem, and there is no published-package route in the README. If your deployment pipeline cannot build from two local checkouts, or if you need a pip install of a single distribution, this project does not offer that path today.

The second is operational maturity. At version 0.1.0 with no releases listed and no documented schema migration or rollback, the upgrade story is manual. Teams that need a versioned upgrade path with tested migrations should look at the runtime layer directly, or wait.

The third is scope. Tiri is a personal-agent shell: one local backend, one SQLite file, one workspace, a small tool set. It is not a multi-tenant agent platform. The README documents no user accounts, no per-tenant isolation, no queueing between concurrent sessions and no horizontal scaling. If your requirement is many users sharing one deployment, the architecture described here is the wrong shape, and no amount of configuration changes that.

A fair comparison for the second and third points is a general-purpose agent framework such as LangChain or a hosted assistant platform. Those take the opposite approach: they give you composable pieces or a managed service and leave the product shell to you, with a much larger surface area and, in the hosted case, someone else running the servers. Tiri's value is that the shell already exists, and its cost is that you inherit this particular shell's constraints.

Editorial conclusion

Adopt Tiri Agent if you want a self-hosted agent with a real web shell and chat channels and you are comfortable installing a pinned sibling package from a local path. Do not adopt it if you need a published PyPI install, a stable release line, or a permissive licence for closed distribution, because the project is at version 0.1.0 and ships under AGPL-3.0-only. Before committing, confirm that agent-runtime 0.2.5 is available to you, decide whether TIRI_SANDBOX_BACKEND should be local or local-docker, and check what ~/.tiri/tiri.db will hold.

Frequently asked questions

How do I install Tiri Agent?

The README's quick start creates a Python 3.11 virtual environment with uv and installs both the sibling agent-runtime package and this repository from local paths, including the [server] extra. It then installs pytest. There is no documented install from a published package index.

Which model providers does Tiri Agent support?

The README shows TIRI_LLM_API set to openai-chat-completions with TIRI_MODEL, TIRI_LLM_API_KEY and an optional TIRI_LLM_BASE_URL, which is the pattern used for OpenAI-compatible endpoints. No other provider API shapes are listed in the README.

Where does Tiri Agent store its data?

Durable state lives in SQLite, by default at ~/.tiri/tiri.db, and the README says it holds chat sessions, message history, event-stream records, local settings, channel settings, proactive targets and outbox entries. TIRI_DATA_DIR and TIRI_DB_PATH change the location.

Official sources

  1. easylink-ai-open/tiri-agent on GitHub
  2. Issues
  3. License: AGPL-3.0
  4. README
Community notes

Community notes