Architecture
TAI is a pnpm monorepo with a small runtime core, a host CLI, a web surface, and first-party plugins that use the same extension contracts available to third parties.
The defining ideas
- One
AgentRuntime. The runtime owns the config, the tool registry, the LLM provider, and the SQLite handle. Everything else borrows from it. The CLI, the HTTP server, the Discord channel, cron, and the workflow engine all read and write through the same instance. - Durable state lives in SQLite. Sessions are scoped to their channel; tasks, projects, rooms, schedules, and memory are shared deliberately through their own records instead of leaking one chat history everywhere.
- The agent loop is one function.
chat → tool calls → chat → stop. No state machine. No DAG. Compaction, retries, and hooks wrap around it. - Hot reload by default. Config, tools, and provider are references
the runtime re-resolves every iteration. Edit
config.yaml, save, and the next message uses the new config. - Channels are registered transports. Discord is built in and Slack is a first-party plugin. Each turns an external message into an agent run and routes output back through a transport-neutral outbound registry.
Primary packages
| Package | Path | What's in it |
|---|---|---|
@tailored-ai/core | packages/core/ | Runtime, config, tools, providers, channels, db, cron, hooks, workflows, memory. |
@tailored-ai/server | packages/server/ | HTTP API (Hono), SSE streaming, webhook intake, optional static-UI mount. |
@tailored-ai/cli | packages/cli/ | The tai command. REPL, one-shot, project mode, bundled web UI. |
@tailored-ai/browser-mediator | packages/browser-mediator/ | Bounded browser tool. Framework-agnostic; OpenAI / Anthropic / TAI adapters. |
@tailored-ai/trusted-actions | packages/trusted-actions/ | HITL approval gateway and executor. |
@tailored-ai/ui (private) | packages/ui/ | React + Vite SPA. Bundled inside @tailored-ai/cli. |
@tailored-ai/site (private) | packages/site/ | This documentation site. |
See Packages overview for the dependency graph.
The runtime
┌──────────────────────────────────────────────────────┐
│ AgentRuntime │
│ │
│ Config Tools DB Provider │
│ (YAML) (Tool[]) (SQLite) (LLM API) │
│ │
└──────────────────────────────────────────────────────┘
▲ ▲ ▲
│ │ │
┌────┴───┐ ┌─────┴────┐ ┌────┴─────┐
│ CLI │ │ Discord │ │ HTTP │
│ REPL │ │ channel │ │ + SSE │
└────────┘ └──────────┘ └──────────┘
The runtime is constructed once. Every entry point calls
runtime.buildLoopOptions({ session, agentName, project }) and hands the
result to runAgentLoop(content, opts). The same five fields above
(config, tools, db, provider, plus a mutable cache) are read each
iteration.
The agent loop
loop:
send (system, history, tools) → provider
receive (text | tool_calls)
if tool_calls:
validate args
execute each tool with retry on transient errors
append tool_results to history
continue
else:
return text
The loop stops when the model returns plain text or hits
maxToolRounds. Around the loop sit history compaction (drops older
messages when the context window fills), prompt expansion
({{next_task}}, {{last_run_iso}}), beforeRun and afterRun hooks, and
per-tool retry with backoff.
Source: packages/core/src/agent/loop.ts.
Persistence
One SQLite file. Its configured path is relative to the instance home, which is
~/.tailored-ai/ by default or the directory selected by $TAI_HOME / -c.
| Table | What's in it |
|---|---|
sessions | One row per chat. User key, model, provider, project_id, timestamps. |
messages | Every turn in every session. Tool calls and results inlined. |
project_tasks | The native task backend (status, assignee, tags). |
task_comments | Comments per task. |
projects | Registered projects, for per-project mode. |
notes | The recall memory tier: short observations, tags, embeddings. |
chunks | Promoted longer-form content with embeddings. |
email_seen | Dedup ledger for the gmail tool. |
cron_runs | History of cron job executions. |
agent_schedules | Persistent one-shot and recurring wakes agents booked for themselves. |
rooms, room_subscriptions, room_messages | Shared-room definitions, membership, cursors, and transcripts. |
workflow_runs, workflow_steps | Workflow telemetry. |
digest_runs | Autopilot morning-digest archive. |
exploratory_runs | Online tick history (one row per tick). |
autopilot_settings | Digest time, memory-sweep TTL. |
actions, subscriptions, audit_log | Trusted-actions executor (if installed). |
better-sqlite3 is synchronous. The hot path uses prepared statements.
Factories
packages/core/src/factories.ts is the composition layer. Three pure
builders:
createTools(config, contextDir, configPath, opts)builds enabled tools from the registered factories andconfig.tools.*blocks.createProvider(config, providerId?)resolves a registered provider id and returns its provider plus default model.createMetaTools(opts)returns tools that depend on a runtime reference:delegate,task_status,admin,claude_code,discord_dm.
The CLI loads registry plugins, builds the AgentRuntime, then starts channels,
HTTP, cron, schedules, rooms, workflows, MCP, and background workers against
that shared runtime.
Online mode
An agent with online: { enabled: true } runs a background tick on a cadence
when the global exploratory.enabled switch is also on. Each tick reads the
agent's goals.md, runs the loop with a narrowed tool allowlist and budgets,
and either acts or backs off. The default cadence starts at 30 minutes and
doubles after no-op ticks up to four hours.
The runtime calls this online mode externally. The internal module is
packages/core/src/exploratory/; the rows in exploratory_runs are the audit
trail. See Online agents for configuration and limits.
There's a separate autopilot worker (packages/core/src/autopilot/)
for cron-like jobs that aren't tied to a user message: the morning
digest (8am by default), the daily memory sweep (3:14am), the
stuck-task scan (every 15 minutes). Autopilot doesn't run an agent
loop; it executes SQL.
Sandboxes and worktrees
Tool execution that touches the host (exec, write) can be
sandboxed. Three backends:
| Backend | When to use |
|---|---|
host | No sandbox. Tools run in the same process. The default. |
docker | createSandbox({ kind: "docker", image: "node:20" }). Commands run inside a container. |
podman | Same shape as docker, rootless. |
Used by the coder workflow: writes code in an isolated git worktree,
runs typecheck and tests in a Docker sandbox, then offers the branch for
review. Worktrees are managed via
createWorktree({ strategy }).
Deep dive: docs/sandboxes-and-worktrees.md.
Layout
packages/
├── core/src/
│ ├── index.ts # barrel exports
│ ├── runtime.ts # AgentRuntime
│ ├── factories.ts # createTools / createProvider / createMetaTools
│ ├── config.ts # YAML loader, validation, types
│ ├── context.ts # context / memory file loader
│ ├── agent/ # loop, agents, session, compact, hooks, prompt
│ ├── providers/ # openai_compatible, embedding, registry
│ ├── tools/ # built-in tools + interface
│ ├── channels/ # discord, interface
│ ├── triggers/ # file-drop, email-poll, rss-poll, calendar-poll, etc.
│ ├── memory/ # recall tier, embeddings, sweep
│ ├── workflows/ # loader, engine, executors
│ ├── tasks/ # backends: native, github, beans, beads
│ ├── projects/ # per-project resolution
│ ├── sandboxes/ # host, docker, podman
│ ├── cron/ # scheduler + cron_runs
│ ├── autopilot/ # digest, memory sweep, stuck-task scan
│ └── exploratory/ # online tick worker
├── server/src/index.ts # Hono routes
├── cli/src/ # arg parsing, REPL, service orchestration
├── browser-mediator/src/ # mediator, egress, sanitizer, hitl, adapters
└── trusted-actions/src/ # gateway, executor, adapters