A context-efficient agentic assistant framework. Connect it to your LLM, configure a bridge (CLI, Discord, Matrix, or HTTP gateway), and you have a persistent, tool-using AI agent with memory consolidation, a knowledge graph, user permissions, scheduled heartbeats, subagent support, and web browsing.
- Effortless onboarding wizard
- Optimised for local LLMs - 32k context recommended, 16k workable
- Branch-backed conversation tree persisted in SQLite - no state lost on restart
- Active memory consolidation and background knowledge-graph extraction (LadybugDB)
- Semantic search over your notes: BM25 or hybrid BM25 + embeddings
- User permission system (0-100) managed via the internal user registry
- Terminal UI with persistent session restore, slash commands, paste refs, and copy helpers
- Direct web browsing via
web_searchandopen_url(Playwright, headless by default) - Proactive heartbeat and cron system
- Subagent support (
spawn_agent/wait_agent) - MCP server integration
- Provider presets for OpenAI, OpenRouter, Ollama, LM Studio, llama.cpp, and any OpenAI-compatible endpoint
Warning
Security notice - read before exposing to a network.
TinyCTX gives the agent real tools: shell execution, file read/write, and web access. Any user who can reach the bot can instruct the agent to use these tools. By default, bridges accept messages from everyone.
Before enabling any network bridge (Discord, Matrix, gateway), decide who is allowed to talk to the bot and configure accordingly:
allowed_servers/room_ids- restrict which servers or rooms the bot responds in. An emptyallowed_serversmeans the bot won't respond in any server.- Permission levels - every user has a level (0-100) stored in the TinyCTX user registry. New users start at 0. Tools declare a
min_permissionthreshold; callers cannot execute tools above their level. Grant higher levels with/user grant <username> <level>(requires level 100). Setdm_requires_permissionandreset_requires_permissionin the Discord bridge options to control who can DM the bot and who can/reset. prefix_required: true- in group channels, only respond when @mentioned or prefixed. This reduces noise but is not a security boundary on its own.- Gateway
api_key- always set a strong, random key if the gateway is enabled. Never expose the gateway port to the public internet without authentication.
The filesystem module sandboxes write_file/edit_file to the workspace directory only. view/grep/glob_search can additionally see any directory listed in filesystem.read_only_paths in config.yaml (read-only, never writable - e.g. /app for the agent's own source code in the container). Nothing outside workspace + that whitelist is reachable at all. The shell module parses commands with tree-sitter-bash and checks each one against a YAML policy, but these are last-resort guardrails, not a substitute for access control - the sandbox container is the real security boundary.
The right mental model: treat TinyCTX like an SSH session. Only give access to people you'd give a shell to.
git clone https://github.com/itzpingcat/TinyCTX
cd TinyCTX
pip install -e .
python -m TinyCTX onboardThis starts the interactive configuration wizard. It will walk you through choosing a provider, configuring your workspace, and optionally setting up bridges.
An instance is a self-contained directory holding one agent's config, workspace, and internal data - everything an agent needs lives in one place, so running multiple agents is just running multiple instance directories.
Every tinyctx command resolves the instance directory the same way:
--dir PATH, if given- The nearest ancestor of your current directory that's literally named
.tinyctx(so running from inside<instance>/workspace/skills/foostill resolves correctly) - A
.tinyctx/child of your current directory - Fallback:
~/.tinyctx
<instance>/
+-- config.yaml # loaded from here by default
+-- workspace/ # agent-authored content - visible to the agent's own filesystem tools
| +-- SOUL.md # Agent personality - loaded first, every turn
| +-- AGENTS.md # Sub-agent or persona definitions
| +-- EM.md # Equipment manifest (optional; templated with OS/date/paths)
| +-- HEARTBEAT.md # Heartbeat instructions (read by agent each heartbeat turn)
| +-- rag/, memory/*.md # Semantic search corpus - any *.md files here are searchable
| +-- downloads/ # Files and images sent by users via bridges
| +-- CRON.json # Scheduled jobs (cron module)
| +-- skills/
| +-- mytool/
| +-- SKILL.md
+-- data/ # TinyCTX-internal state - NOT visible to the agent's own filesystem tools
+-- agent.db # Branch-backed conversation tree (SQLite WAL)
+-- users.db # User registry
+-- cursors/ # Per-bridge/session cursors (CLI resume uses this)
+-- memory/
+-- graph.lbug # LadybugDB knowledge graph (memory module)
+-- librarian.log # Librarian logging
+-- dedup_cache.db
workspace.path and data.path both default to <instance>/workspace and <instance>/data - relative to wherever config.yaml itself lives - so a fresh config.yaml doesn't need to state either explicitly. Override only if you want something non-standard.
Edit files under workspace/ any time - they are re-read every turn, no restart needed. Files under data/ are internal state the agent's own tools can't reach; edit them only if you know what you're doing.
TinyCTX does not keep chat state only in RAM. Conversations are stored in data/agent.db as a branch tree, and the CLI bridge restores the visible transcript from the saved cursor on startup.
TinyCTX is designed to work within a fixed context window rather than silently discarding history. Set context: in config.yaml to match your model:
context: 32768 # recommended; 16384 works for smaller modelsWhen the active turn approaches this limit, TinyCTX trims the oldest non-system turns. The memory and RAG modules then pick up the slack - important facts are preserved in the knowledge graph or semantic index and re-injected as needed. The full conversation tree is always on disk.
TinyCTX has three complementary memory systems.
These files are always injected every turn:
SOUL.md- agent personalityAGENTS.md- roles, personas, or sub-agent definitionsTOOLS.md- tool usage guidelines
Any .md files placed under workspace/rag are indexed. They can be configured to be searched automatically each turn. The most relevant chunks are injected into context. Subdirectories are supported.
To enable embedding-based (semantic) search, add an embedding model:
models:
embed:
kind: embedding
base_url: http://localhost:11434/v1
api_key_env: N/A
model: nomic-embed-text
memory_search:
embedding_model: embedWithout an embedding model, BM25 keyword search is used - no extra server required.
The agent can also call rag_search explicitly to look things up on demand. See example.config.yaml under rag: for all options (chunk strategy, budget, top-k, auto-inject, etc.).
The memory module adds a property-graph knowledge store backed by LadybugDB. A background librarian process walks unvisited conversation nodes (tracked with DB flags), extracts entities and relationships via sub-agents, and writes them to data/memory/graph.lbug. The main agent reads the graph via kg_search, kg_traverse, and call_librarian tools. Pinned entities are injected into the system prompt automatically.
# memory module (all optional - these are the defaults)
# graph_path: data/memory/graph.lbug # resolved relative to data.path, not workspace.path
# trigger_interval_hours: 6
# batch_size: 20
# embedding_model: "" # empty = keyword-only graph search
# memory_block_tokens: 4096
# librarian_model: "" # empty = use primary LLMEvery inbound message is associated with a User - a TinyCTX-internal account that may have identities on multiple platforms (Discord, Matrix, CLI, etc). Users are created automatically on first contact and stored in <instance>/data/users.db.
Each user has a permission level (0-100). Tools declare a min_permission threshold; a caller cannot execute tools above their level. New users start at default_permission (default: 25).
/user grant <username> <level> - requires caller level 100
/user info <username>
/user rename <username> <new>
By default (permissions.minimal_tokens: true), the LLM only sees tools the current caller has permission to execute - higher-privilege tools are hidden entirely, saving tokens and avoiding confusion. Set minimal_tokens: false to show all tools; execution-time guards still apply.
Skills are reusable instruction sets the agent can load on demand. Place a folder containing a SKILL.md file anywhere under workspace/skills/.
The agent sees a compact index of available skills and calls use_skill("name") to load the full instructions when needed. Skills follow the agentskills.io convention.
TinyCTX can spawn bounded child branches for parallel side work:
spawn_agent(prompt="...") - start a detached subagent
wait_agent(task_id="...") - wait for it to finish or poll status
Good for isolated side tasks; not worth the overhead for trivial work you can finish in the current turn.
Any stdio MCP server can be wired in via config:
mcp:
servers:
filesystem:
command: npx
args: ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
tools:
read_file: always_on
write_file: deferred
delete_file: disabledPer-tool visibility: always_on | deferred (hidden until tools_search enables it) | disabled. All tools default to deferred if no tools: block is present.
| Tool | What it does |
|---|---|
shell |
Run a shell command in the workspace directory |
view |
Read a file with line numbers, or list a directory |
write_file |
Create or write to a file (append, prepend, overwrite) |
edit_file |
Edit an existing file by replacing a string |
grep |
Search file contents with regex (ripgrep with Python fallback) |
glob_search |
Find files by name pattern, sorted by modification time |
web_search |
Search the web via DuckDuckGo |
open_url |
Open any URL in a Playwright browser; headless=False for captchas |
memory_search |
Search the RAG semantic index |
kg_search |
Search the LadybugDB knowledge graph |
kg_traverse |
Traverse graph relationships from a starting entity |
call_librarian |
Trigger on-demand knowledge-graph extraction |
spawn_agent |
Start a detached subagent on a child branch |
wait_agent |
Wait for a spawned subagent to finish or poll its status |
use_skill |
Load a skill by name |
todo_write |
Update the session task checklist |
todo_read |
View the current task list |
present |
Deliver files to the user via the active bridge |
tools_search |
BM25 search over available tools; enables matching deferred tools |
Write tools (write_file, edit_file) require the file to have been read first via view() - this prevents blind overwrites.
Full annotated config: see example.config.yaml. Key top-level keys:
| Key | Default | Purpose |
|---|---|---|
context |
16384 |
Token budget (recommend 32768) |
max_tool_cycles |
10 |
Max tool-call iterations per turn |
workspace.path |
<instance>/workspace |
Agent-visible working directory |
data.path |
<instance>/data |
Internal state (agent.db, users.db, memory graph, cursors) - not visible to the agent's own filesystem tools |
filesystem.read_only_paths |
[] |
Extra directories view/grep/glob_search can see (never write to) - e.g. /app |
llm.primary |
- | Primary model name (must be kind: chat) |
llm.fallback |
[] |
Fallback model names, tried in order |
permissions.minimal_tokens |
true |
Hide tools the caller cannot use |
gateway.api_key |
- | Auth token for the HTTP gateway |
gateway.port |
8085 |
Overridable per-instance via TINYCTX_PORT env (set automatically by tinyctx start) |
Models are defined under models: with kind: chat (default) or kind: embedding. Embedding models are never used for LLM routing.
Interactive terminal session with Rich TUI, slash commands, and persistent session restore.
tinyctx launch clibridges:
discord:
enabled: true
options:
token_env: DISCORD_BOT_TOKEN
allowed_servers:
987654321098765432: [] # all channels in this server
dm_enabled: true
dm_requires_permission: 75 # minimum user registry level to DM the bot
reset_requires_permission: 75 # minimum level to /reset in a server
prefix_required: true
command_prefix: "!"Required bot intents: Message Content, Server Members. Required permissions: Read Messages, Send Messages, Read Message History.
bridges:
telegram:
enabled: true
options:
token_env: TELEGRAM_BOT_TOKEN # env var holding the @BotFather token
allowed_users: [123456789] # Telegram user IDs; empty = open to all
max_reply_length: 4096 # Telegram's hard per-message limit
mention_aliases: ["eve"] # extra names the bot answers to in groupsCreate a bot with @BotFather and export the token as TELEGRAM_BOT_TOKEN. Run the bridge like the others: python -m TinyCTX.bridges.telegram. In groups the bot answers to its @username, its BotFather display name, a reply to one of its own messages, and any mention_aliases; bare-name matching requires privacy mode disabled in @BotFather (/setprivacy, then Disable, then re-add the bot to the group).
bridges:
matrix:
enabled: true
options:
homeserver: https://matrix.org
username: "@yourbot:matrix.org"
password_env: MATRIX_PASSWORD
allowed_users: ["@you:matrix.org"]
default_permission: 25
power_level_map:
100: 100
50: 50
0: 25Requires matrix-nio (pip install matrix-nio, or matrix-nio[e2e] for E2EE).
OpenAI-compatible /v1/chat endpoint with SSE streaming. Useful for external clients and SillyTavern.
gateway:
enabled: true
host: 127.0.0.1
port: 8085
api_key: "your-secret-token"tinyctx onboard # first-run setup wizard for the resolved instance
tinyctx start # start the stack (docker compose) for the resolved instance
tinyctx stop # stop it
tinyctx status # check if running
tinyctx launch cli # attach an interactive terminal sessionAll five accept --dir PATH to target a specific .tinyctx instance directory instead of relying on autodetection - this is how you run more than one agent on the same machine: give each its own instance directory (e.g. tinyctx onboard --dir ~/agent-b/.tinyctx), then use --dir (or cd into it) for every subsequent command against that instance.