Multi-agent CLI that answers questions about your database and runs code in a sandbox.
Project description
Terno Agent
A single-agent coding CLI + SDK. The agent reads and edits files, runs shell commands, executes Python in a sandbox, tracks its own task list, spawns subagents for parallel work, and pulls in additional tools from any Model Context Protocol (MCP) servers you configure.
Features
- One agent, one prompt. No multi-agent orchestration — a single
TernoAgentpowered by Anthropic Claude or OpenAI. - Built-in tools:
read_file,write_file,edit_file,bash,run_python,task_create/task_list/task_get/task_update,spawn_agent,ask_user,search_memory. - Edits show coloured diffs. Every
edit_filecall renders as a unified diff in the CLI (red removals, green additions, cyan hunk headers).write_filerefuses to clobber existing files unless you passoverwrite=true— and when you do, the panel shows the diff between the on-disk content and the proposed rewrite. Edits become hard to miss; surprise overwrites become impossible. - Permission prompts for tool use. A
pre_tool_usehook gates every tool call. The CLI prompts with three Claude-style options: allow once, allow this tool for the rest of the session, or deny and tell the agent what to do instead (your reason goes straight back to the LLM as a tool-result error). Read-only helpers (read_file,task_*,search_memory,ask_user) skip the prompt. - Human-in-the-loop questions.
ask_userlets the agent pause and pose 1–4 multiple-choice questions when the request is genuinely ambiguous. Each question gets 2–4 options plus an automatic "Other (custom text)" choice, single- or multi-select. The CLI walks the user through them one at a time. - Sandboxed Python.
run_pythonruns inside Docker by default (--network none, read-only rootfs, mem/CPU caps); a local subprocess sandbox is available for dev. The tool is auto-hidden when no sandbox is reachable. The sandbox layer is pluggable — third-party backends (QEMU, browser-based, vendor APIs) register via Python entry points; see Sandbox plugins. - MCP support. Drop a
.terno/mcp.jsonin your repo (Claude-Code-compatible format) and every remote tool shows up asmcp__server__tool. Servers can be launched viauvx,npx, or Docker, or connected to over HTTP/SSE. See MCP. - Agent Skills. Terno ships common built-in skills for data science
and general-purpose work, and you can add
SKILL.md-based skills in.terno/skills/,.agents/skills/, or.claude/skills/. Either Anthropic or OpenAI models load full instructions throughactivate_skillonly when needed. - Subagent spawner.
spawn_agentrecursively launches a freshTernoAgentwith a caller-supplied system prompt — useful for isolating focused subtasks from your main context. - Persistent memory. After each turn a background extractor mines
the user's question and the assistant's answer for facts worth
keeping — user preferences, project context, feedback, references,
and short Q&A insights stored as key→value pairs. Everything lands
in
<workdir>/.terno/memoryas markdown + vectors. On the next turn, the most relevant entries are recalled into context. The CLI prints a single dimmemory updatedline when something changed; the extractor itself runs silently. See Memory. - Streaming + typed events. Assistant text streams live; tool calls and results render with syntax-highlighted panels.
- Native attachments. Attach text files, documents, or images to a turn; Terno stores them locally, sends images through provider vision payloads when available, and keeps large text files bounded with selected chunks instead of dumping whole files into the prompt.
- CLI + library.
terno ask "..."/terno chatfrom the shell, orfrom terno import Agentin Python. - Deep research (database). A four-phase pipeline (org context,
schema crawl, semantic annotation, validation) builds a queryable
knowledge base from any database — run it as
terno deep_researchor from insideterno chatvia/deep_research.
Architecture
┌──────────────────────────┐
user task → │ TernoAgent │
│ (single sync run loop) │
└────────────┬─────────────┘
│
┌─────────────────────────┼─────────────────────────┐
▼ ▼ ▼
built-in tools spawn_agent MCP tools
read_file (fresh TernoAgent, (loaded from
write_file (gated) shares manager + .terno/mcp.json,
edit_file (diff'd) task store) via uvx /
bash npx / docker
run_python (sandbox) / HTTP / SSE)
ask_user (HITL)
search_memory
activate_skill
task_* (in-memory)
every tool call passes through a pre_tool_use hook
→ permission prompt in CLI / SDK hook in code
All cross-cutting boundaries are protocols, so each layer is swappable:
| Boundary | Protocol | Implementations |
|---|---|---|
| LLM | LLMClient |
Anthropic, OpenAI |
| Sandbox | Sandbox |
Docker, local subprocess |
| Tool | Tool |
file ops, bash, run_python, tasks, MCP |
| Database | SQLAlchemy | only used by deep_research |
Install
You can install with either uv or plain pip. Both produce the same
terno CLI on your PATH and the same importable terno_agent package.
Optional extras
| Extra | What it pulls in |
|---|---|
anthropic |
the anthropic SDK |
openai |
the openai SDK |
docker |
the docker SDK for sandboxing |
mcp |
the official mcp Python client |
postgres |
psycopg[binary] |
mysql |
pymysql |
all |
all of the above |
dev |
pytest, ruff, mypy |
With uv (recommended)
# install globally as a uv tool — `terno` works from anywhere
uv tool install terno-agent
uv tool install "terno-agent[anthropic,docker,mcp]"
# editable install from a local checkout
git clone https://github.com/terno-ai/terno-agent.git
cd terno-agent
uv tool install --editable ".[all]"
# refresh after editing pyproject.toml
uv tool install --editable ".[all]" --force
With pip
pip install terno-agent
pip install "terno-agent[anthropic,docker,mcp]"
# editable
git clone https://github.com/terno-ai/terno-agent.git
cd terno-agent
python -m venv .venv && source .venv/bin/activate
pip install -e ".[all]"
If you install into a project venv with plain
pip, activate it (or use.venv/bin/terno).uv tool installavoids this by giving the CLI its own isolated environment onPATH.
Configure
Configuration is read from environment variables, with .env auto-loaded
from the current working directory or any parent. Process env wins over
.env.
cp .env.example .env
# then edit:
ANTHROPIC_API_KEY=sk-ant-... # or OPENAI_API_KEY=
TERNO_LLM_PROVIDER=anthropic # anthropic | openai
TERNO_LLM_MODEL=claude-opus-4-7
TERNO_SANDBOX=docker # docker | local | none | <plugin name> | pkg.mod:Cls
# When the primary sandbox can't initialize, try this one (default: local).
# Set to 'none' to disable fallback.
TERNO_SANDBOX_FALLBACK=local
# Keep the sandbox container alive after the session ends so the next
# `terno` invocation in the same cwd attaches to it (vars + imports + /work
# files all preserved).
TERNO_SANDBOX_PERSIST=false
# Override the auto-derived per-cwd container name. Only used when persist=true.
# TERNO_SANDBOX_CONTAINER_NAME=my-sandbox
# Optional kwargs forwarded to the sandbox constructor:
# TERNO_SANDBOX_OPTIONS=image=python:3.13,memory=1g
# optional — only needed for `terno deep_research`
TERNO_DATABASE_URL=sqlite:///./demo.db
# optional — MCP loading is on by default. The project default is
# `.terno/mcp.json`; setting TERNO_MCP_CONFIG points at a specific file
# (which is loaded *instead* of the default for env-based discovery).
TERNO_MCP_ENABLED=true
TERNO_MCP_CONFIG=/path/to/mcp.json
# optional — Agent Skills are on by default
TERNO_SKILLS_ENABLED=true
# TERNO_SKILL_PATHS=/path/to/skills
# optional — memory is on by default; needs an OpenAI key for embeddings
TERNO_MEMORY_ENABLED=true
TERNO_MEMORY_TOP_K=5
TERNO_EMBEDDING_MODEL=text-embedding-3-small
# TERNO_EMBEDDING_API_KEY= # falls back to OPENAI_API_KEY
Run terno config to print the effective settings (API keys masked).
Use the CLI
# one-shot task
terno ask "refactor utils.py into smaller modules"
# attach files to a one-shot task
terno ask --attach report.pdf --attach chart.png "summarize these"
# interactive REPL
terno chat
# in chat, queue files for the next turn
/attach report.pdf
/attachments
# suppress streaming/activity, print only the final answer
terno -q ask "explain how config.py loads .env files"
# four-phase deep research over the configured database
terno deep_research
# show effective config
terno config
# show version
terno --version
If you installed with plain pip into a project venv and didn't activate it:
.venv/bin/terno ask "..."
# or
python -m terno_agent ask "..."
What you'll see in chat
The first time the agent reaches for any side-effecting tool (bash,
edit_file, write_file, run_python, spawn_agent, MCP tools…)
you'll get a permission prompt:
╭─ permission required ──────────────────────────╮
│ Tool: bash │
│ │
│ Arguments: │
│ { "command": "uv run pytest -q" } │
╰────────────────────────────────────────────────╯
[1] Allow once
[2] Allow 'bash' for the rest of this session
[3] Deny and tell the agent what to do instead
permission> 2
Picking (2) skips future prompts for that tool name in this session.
(3) asks for a free-text reason that's sent back to the agent as a
tool-result error — the model adapts instead of being silently blocked.
Read-only tools (read_file, task_*, search_memory, ask_user)
skip the prompt entirely.
When the agent edits a file you get a coloured unified diff in place of the raw arguments:
╭─ [terno] → edit_file ──────────────────────────╮
│ path: src/utils.py │
│ --- a/src/utils.py │
│ +++ b/src/utils.py │
│ @@ -10,3 +10,3 @@ │
│ def add(a, b): │
│ - return a+b │
│ + return a + b │
╰────────────────────────────────────────────────╯
If the agent needs a decision from you it can call ask_user, which
walks through one or more multiple-choice questions:
╭─ Question 1/2 ─────────────────────────────────╮
│ Which database driver should I use? │
╰────────────────────────────────────────────────╯
[1] psycopg (sync) — default for scripts
[2] asyncpg — for the async data loader
[3] Other (custom text)
select> 1
And whenever the background memory extractor commits something new you'll see a single dim line:
memory updated
Use as a library
from terno import Agent is the SDK entry point. Agent is a thin
facade — the inference loop, tool dispatch, MCP manager, and memory
pipeline all live behind it. You drive the agent via one method
(run / ask), receive results as an AgentRun dataclass, and
optionally subscribe to a stream of typed events.
Quick start
from terno import Agent
# All kwargs are optional — anything missing falls back to env / .env.
agent = Agent(api_key="sk-ant-...")
result = agent.run("read README.md and summarize what the agent does")
print(result.answer)
result = agent.run(
"compare these files",
attachments=["./report.pdf", "./chart.png"],
)
Agent(...) accepts api_key, provider, model, database_url,
config, and on_event. For everything else (sandbox, MCP, memory),
build a Config and pass it via config=.
Constructors
from terno import Agent
from terno_agent.config import Config
# 1. Inline kwargs (simplest). Missing fields read from env / .env.
agent = Agent(
api_key="sk-ant-...",
provider="anthropic", # "anthropic" | "openai"
model="claude-opus-4-7",
)
# 2. Everything from environment + .env.
agent = Agent.from_env()
# 3. Explicit Config — exposes the full surface (sandbox, MCP, memory…).
config = Config(
llm_provider="anthropic",
llm_api_key="sk-ant-...",
sandbox="local", # "docker" | "local" | "none"
mcp_enabled=False, # skip .terno/mcp.json
memory_enabled=True, # persistent recall
memory_top_k=5,
embedding_provider="openai",
embedding_api_key="sk-openai-...",
)
agent = Agent.from_config(config)
Running tasks
run(task) and ask(task) are equivalent. Both block until the agent
emits a final assistant message (no remaining tool calls) and return an
AgentRun:
result = agent.run("count Python files under src/")
result.answer # str — final assistant message
result.iterations # int — LLM turns taken
result.trace # list[Message] — full conversation incl. tool calls
Each call starts a fresh conversation (new system prompt + your task).
The agent keeps task-tracking state (task_list) across calls within
one Agent instance.
Lifecycle — always close when done
If you configured MCP servers or memory (both on by default), the agent owns background resources: subprocesses for stdio MCP servers, an asyncio loop on a worker thread, OpenAI clients. Use it as a context manager so they shut down cleanly:
from terno import Agent
with Agent.from_env() as agent:
print(agent.run("...").answer)
print(agent.run("...follow-up").answer)
Or call agent.close() explicitly. Both are idempotent; close() is
also registered with atexit as a defensive net.
Streaming events
Pass on_event= to receive typed events as the agent works. The hook
runs synchronously inside the agent loop — keep it fast.
from terno import Agent
from terno_agent.core.events import (
IterationStart, TextDelta, ToolCallEvent, ToolResultEvent, TurnEnd,
)
def on_event(e):
if isinstance(e, TextDelta):
print(e.text, end="", flush=True)
elif isinstance(e, ToolCallEvent):
print(f"\n[tool] {e.call.name}({e.call.arguments})")
elif isinstance(e, ToolResultEvent):
marker = "✗" if e.result.is_error else "✓"
print(f" {marker} {e.result.content[:200]}")
# IterationStart and TurnEnd are also available
with Agent(api_key="sk-ant-...", on_event=on_event) as agent:
agent.run("explain src/terno_agent/agents/terno.py in two sentences")
| Event | When |
|---|---|
IterationStart |
start of each LLM call |
TextDelta |
streamed token chunk from the assistant |
ToolCallEvent |
the LLM picked a tool, before it runs |
ToolResultEvent |
tool returned (carries success / error) |
TurnEnd |
LLM call finished, message appended |
Disabling MCP or memory in code
.terno/mcp.json is loaded by default if present. To skip it for a single
run without touching the file:
from terno_agent.config import Config
config = Config.from_env()
config.mcp_enabled = False # don't load .terno/mcp.json
config.skills_enabled = False # don't discover Agent Skills
config.memory_enabled = False # no recall, no extraction
agent = Agent.from_config(config)
Agent Skills
Terno supports Agent Skills using the standard SKILL.md shape: a skill
is a directory containing a required SKILL.md file with YAML
frontmatter (name and description) followed by Markdown
instructions. At startup, Terno loads only each skill's metadata into the
system prompt. When the model decides a skill is relevant, it calls
activate_skill(name) to load the full instructions and a capped list of
bundled resources.
Built-in skills are available by default:
| Skill | Use for |
|---|---|
code-review |
code reviews, regressions, missing tests |
debugging |
failing tests, runtime errors, flaky behavior |
data-analysis |
dataset exploration, summaries, metrics |
data-cleaning |
messy data, deduplication, standardization |
data-visualization |
charts, dashboards, visual summaries |
documentation |
README, API docs, runbooks, tutorials |
machine-learning |
models, experiments, metrics, leakage checks |
python-data |
dataframe, notebook, numerical, and file-based analysis |
research-synthesis |
research, comparisons, decision briefs |
sql-analysis |
analytical SQL, joins, cohorts, funnels |
task-planning |
multi-step planning, milestones, risks |
Custom discovery checks these roots, with later roots overriding earlier ones. That means project skills can replace built-in skills with the same name:
built-in packaged skills
~/.terno/skills/
~/.agents/skills/
~/.claude/skills/
<cwd or ancestor>/.terno/skills/
<cwd or ancestor>/.agents/skills/
<cwd or ancestor>/.claude/skills/
Add extra roots with TERNO_SKILL_PATHS (use your OS path separator).
Set TERNO_SKILLS_ENABLED=false to disable skills for a session. The
implementation is provider-neutral: skills are just prompt context plus
a normal tool call, so they work with both anthropic and openai.
Minimal skill:
.agents/skills/code-review/SKILL.md
---
name: code-review
description: Review code for regressions, missing tests, and maintainability. Use when the user asks for a code review.
---
# Code Review
Focus on bugs and behavioral risk first. Report findings with file and
line references, then summarize test coverage.
Deep research over a database
from terno import Agent
agent = Agent(api_key="sk-ant-...", database_url="sqlite:///./demo.db")
report = agent.deep_research()
print("ok" if report.ok else "failed")
from terno_agent import Agentis equivalent —ternois a short re-export of the same SDK.
MCP
The agent reads a .terno/mcp.json file at startup. The format is the same one
Claude Code and Cursor use, so existing configs paste in
unchanged. If the file is missing, MCP loading is a no-op; if a server
fails to start you get a stderr warning and the rest of the agent keeps
running.
Discovery order
- Default (no path passed):
$TERNO_MCP_CONFIGif set, otherwise./.terno/mcp.json. - Explicit path passed (e.g.
Config(mcp_config_path=...)): the explicit file is loaded together with./.terno/mcp.jsonand the two are merged. Servers in the explicit file override the default on name conflict.
Set TERNO_MCP_ENABLED=false to disable MCP entirely.
Tool naming
Every remote tool is registered as mcp__{server}__{tool} so it can't
collide with built-in tools. Server names with characters outside
[A-Za-z0-9_-] are sanitized.
.terno/mcp.json examples
Raw stdio (Claude-Code-compatible — terno invokes the command verbatim):
{
"mcpServers": {
"fetch": {
"command": "uvx",
"args": ["mcp-server-fetch"]
},
"filesystem": {
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-filesystem",
"/Users/me/work"
]
},
"github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": {
"GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_TOKEN}"
}
}
}
}
${VAR} is expanded from your shell env at load time.
Higher-level runner block (terno picks the runtime):
{
"mcpServers": {
"puppeteer": {
"runner": {
"type": "auto",
"package": "@modelcontextprotocol/server-puppeteer",
"image": "mcp/puppeteer:latest",
"docker": {
"mounts": [
{ "source": "/tmp/screenshots", "target": "/screenshots" }
]
}
}
}
}
}
runner.type: auto | uvx | npx | docker | command.
With auto, terno prefers Docker (when both an image and docker are
available), falls back to uvx / npx based on package_type or a name
heuristic.
Web (HTTP / SSE) — no subprocess:
{
"mcpServers": {
"linear": {
"url": "https://mcp.linear.app/sse",
"headers": { "Authorization": "Bearer ${LINEAR_MCP_TOKEN}" }
}
}
}
transport: sse | http. Auto-detected from the URL when omitted
(.../sse → SSE, otherwise streamable HTTP).
Pinning Python for uvx
Some MCP packages have transitive deps without wheels for newer Python
versions. If uvx resolves to a Python that breaks the install, force a
version:
"my-server": {
"command": "uvx",
"args": ["--python", "3.12", "--from", "some-pkg", "the-cmd"]
}
(or set "UV_PYTHON": "3.12" in the server's env block).
Sandbox
run_python executes LLM-generated code inside a sandbox. Two backends
ship in the core package:
docker(default) —--network none, read-only rootfs, memory + CPU caps. If the daemon isn't reachable, the agent automatically falls back tolocalwith a one-line notice. SetTERNO_SANDBOX_FALLBACK=nonefor strict Docker-only behavior.local— direct subprocess. Not a security boundary; only for dev.
Plus a none sentinel that skips sandbox construction entirely.
Session lifetime
The Docker sandbox keeps one container per session and runs a small
Python driver inside it. Each run_python call sends the snippet over
the container's stdin, so:
- Variables, imports, and function definitions persist across calls
within the same
terno chatsession orAgentinstance. - Files written under
/workpersist for the lifetime of the container. - Timeouts and cancellations SIGKILL the container and the next call recreates a fresh one — session state is lost but the agent keeps working.
By default the container is killed and removed when the session ends
(CLI exit, or Agent.close() / context-manager exit). To keep the
container across sessions:
TERNO_SANDBOX_PERSIST=true terno chat
# next session reuses the same per-cwd container:
TERNO_SANDBOX_PERSIST=true terno ask "print(session_token)"
The default container name is terno-sandbox-<8-hex> derived from the
current working directory, so each project gets its own container.
Override with TERNO_SANDBOX_CONTAINER_NAME=my-name if you want
something explicit.
The local backend is stateless — each call spawns a fresh subprocess
— and ignores the persist knobs.
Fallback
TERNO_SANDBOX_FALLBACK (default local) names the backend to try when
the primary sandbox fails to initialize. Empty string or none disables
fallback. The notice prints only when fallback actually fires; the louder
warning is reserved for the case where no sandbox is usable.
Selecting a backend
Env or CLI:
TERNO_SANDBOX=local terno ask "print(1+1)"
terno --sandbox local ask "print(1+1)"
Pass per-backend options:
# As env (CSV of key=value pairs):
TERNO_SANDBOX_OPTIONS="image=python:3.13,memory=1g" terno ask ...
# As CLI (repeatable):
terno --sandbox docker --sandbox-option image=python:3.13 ask ...
TERNO_SANDBOX_IMAGE still works for the docker backend; equivalent to
passing --sandbox-option image=....
Sandbox plugins
Third-party backends (QEMU, browser-based, vendor APIs) register via the
terno_agent.sandboxes entry-point group in their own package's
pyproject.toml:
[project.entry-points."terno_agent.sandboxes"]
qemu = "terno_qemu:QemuSandbox"
After pip install terno-qemu the backend is selectable as
TERNO_SANDBOX=qemu or terno --sandbox qemu. For one-off backends
that don't warrant publishing, point TERNO_SANDBOX at a fully-
qualified import string instead:
TERNO_SANDBOX=my_pkg.module:CustomSandbox terno ask ...
A plugin only needs to satisfy the Sandbox Protocol:
from terno_agent.sandbox import ExecutionResult, Sandbox
class CustomSandbox:
def __init__(self, **options):
# Read whatever options the user passes via TERNO_SANDBOX_OPTIONS
# / --sandbox-option / explicit kwargs.
...
def run_python(
self,
code: str,
*,
timeout_s: int = 30,
env: dict[str, str] | None = None,
) -> ExecutionResult:
...
return ExecutionResult(stdout=..., stderr=..., exit_code=...)
Raise terno_agent.core.exceptions.SandboxError from __init__ if the
backend can't initialize (missing dependency, daemon offline, etc.) —
the agent will warn and continue with run_python disabled, the same
way the built-in Docker check behaves today.
Memory
Memory has three jobs: learn something across turns, store it durably, and recall it cheaply on the next turn.
How it runs
After every turn the agent fires a background extractor (daemon
thread — the user is never blocked). The extractor is a fresh
TernoAgent with the memory CRUD toolset; it reads the just-completed
transcript, decides what (if anything) is worth keeping, then calls
save_memory / delete_memory. Its tool activity is not mirrored
to the CLI — you just see one dim memory updated line when the
extractor actually saved or deleted something. Failures are swallowed
so extraction can never break the user-facing flow.
On the next turn, the user's incoming message is embedded and the top-K most similar memories are recalled into the agent's context as background hints.
Five memory types, one location
| Type | What it stores |
|---|---|
user |
Facts about the human (role, expertise, preferences). |
feedback |
Style / approach rules from the user, with a Why: line. |
project |
Non-obvious state of this repo (initiative, decisions, deadlines). |
reference |
Pointers to external systems — Linear, Slack, Grafana, Confluence. |
insight |
Short Q&A fact distilled from a turn, stored as a key→value pair. |
Everything is one markdown file per memory plus a vector sidecar:
<your-project>/.terno/memory/
MEMORY.md # human-readable index
user-role.md
feedback-testing.md
project-auth-rewrite.md
reference-grafana.md
prod-database-host.md # an insight; name = key, body = value
.vectors.jsonl
Override the location with TERNO_MEMORY_HOME=/some/path (useful in
tests).
Insights — the new bit
insight is the memory type the extractor uses to turn ordinary Q&A
into a cache. If the user asks "where does prod live?" and the
assistant answers "db.terno-prod.us-east-1.rds.amazonaws.com",
the extractor saves an insight named prod-database-host whose body
is the bare hostname. Next time anyone (you or the agent) asks
something similar, search_memory surfaces it without re-derivation.
Insights are intentionally short and factual — never speculation,
never the agent's own opinions.
Tools the agent sees
| Tool | Available to | Notes |
|---|---|---|
search_memory |
main agent | RAG lookup over the store. |
list_memories |
extractor only | Enumerate before saving (prefer UPDATE). |
read_memory |
extractor only | Inspect an existing entry by name. |
save_memory |
extractor only | Create or overwrite by name. |
delete_memory |
extractor only | Remove stale or contradicted entries. |
Configuration
| Env var | Default | Meaning |
|---|---|---|
TERNO_MEMORY_ENABLED |
true |
Master kill-switch. |
TERNO_MEMORY_HOME |
<workdir>/.terno/memory |
Storage location override. |
TERNO_MEMORY_TOP_K |
5 |
Recall budget per turn. |
TERNO_EMBEDDING_API_KEY |
falls back to OPENAI_API_KEY |
Embedding provider key. |
Per-session opt-out: terno --no-memory chat. Embedding requires the
openai extra and an API key; if it's missing the agent prints one
warning and keeps running without memory.
Project layout
src/terno_agent/
__init__.py # public re-exports
cli.py # argparse entry, rich renderer, CliPrompter
# (ask_user), CliPermissionPrompter (pre_tool_use),
# coloured unified-diff renderer for edits
config.py # env + .env-driven Config
core/ # message / tool / event / hook / exception types
# (HookEvent.PRE_TOOL_USE + PreToolUseContext live here)
llm/ # LLMClient protocol + Anthropic + OpenAI (streaming)
agents/ # BaseAgent + the single TernoAgent
prompts/ # the single SYSTEM_PROMPT
tools/ # read_file, write_file (overwrite-gated),
# edit_file, bash, run_python, tasks,
# spawn_agent, ask_user, activate_skill
skills/ # SKILL.md discovery + activate_skill adapter
sandbox/ # Docker + local subprocess runners (for run_python)
mcp/ # .terno/mcp.json parser, runner resolver, async bridge,
# session manager, sync Tool adapter
memory/ # background extractor (silent subagent) + retriever
# + single-dir markdown store at .terno/memory
# + SearchMemoryTool surfaced to the main agent
rag/ # embedding client + file-backed vector store
# (shared infrastructure for memory)
knowledge/ # deep_research pipeline (uses db/ + an LLM)
db/ # SQLAlchemy engine + inspector (knowledge only)
tests/ # pytest suite, including tests/mcp/
Develop
git clone https://github.com/terno-ai/terno-agent.git
cd terno-agent
uv venv --python 3.12
uv pip install -e ".[dev,all]"
uv run pytest -q # tests
uv run ruff check . # lint
uv run ruff format . # format
uv run mypy src # type check
Or with plain pip:
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev,all]"
pytest -q
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file terno_agent-0.2.0.tar.gz.
File metadata
- Download URL: terno_agent-0.2.0.tar.gz
- Upload date:
- Size: 372.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.10.4 {"installer":{"name":"uv","version":"0.10.4","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b4b7881357e8ede73bd0142ac894d93ce84248902a580d1e38ca57286e3c131e
|
|
| MD5 |
ce7ef2b600c702b40e336744241f58db
|
|
| BLAKE2b-256 |
a6ed4ef77b193e9a10d5a3cb60de3acb34aebf182ab52121a298631347842489
|
File details
Details for the file terno_agent-0.2.0-py3-none-any.whl.
File metadata
- Download URL: terno_agent-0.2.0-py3-none-any.whl
- Upload date:
- Size: 156.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.10.4 {"installer":{"name":"uv","version":"0.10.4","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
14eab670ee51c973ddc569ab555d97623f1c8fa3fc50bef8d917819af6f373f9
|
|
| MD5 |
fafa452bd9190f5516e0041873e05ed1
|
|
| BLAKE2b-256 |
6beea3ecc2b56b6be7012d0c222201cbfe387e90559df3ead57c6e2bebef9086
|