New AI Agent Architecture to fix LLM deviations and token costs
Botcircuits is an open-source AI agent that combines LLM step reasoning with a deterministic state machine, enabling token-efficient and predictable multi-step automation. It features a CLI, workflow system with natural language authoring, skills, and MCP support.
Notifications You must be signed in to change notification settings
Fork 1
Star 3
BranchesTags
Open more actions menu
Folders and files
NameName
Last commit message
Last commit date
Latest commit
History
5 Commits
5 Commits
.botcircuits
.botcircuits
.vscode
.vscode
docs
docs
scripts
scripts
skills/botcircuits-faq
skills/botcircuits-faq
src/botcircuits
src/botcircuits
.env.example
.env.example
.gitignore
.gitignore
.python-version
.python-version
IMPLEMENTATION.md
IMPLEMENTATION.md
LICENSE
LICENSE
README.md
README.md
main.py
main.py
pyproject.toml
pyproject.toml
uv.lock
uv.lock
Repository files navigation
The workflow-native AI agent where an LLM handles the reasoning and tool calls for each step, while a deterministic state machine controls the overall flow. The result:predictable and token-efficient multi-step automation without depending on an LLM to drive everything.
Quick Start
Setup
Quick install
Linux, macOS, WSL2, Termux
curl -fsSL https://raw.githubusercontent.com/botcircuits-ai/botcircuits-agent/main/scripts/install.sh | bash
Or Clone and install
1. Install uv (skip if you already have it)
curl -LsSf https://astral.sh/uv/install.sh | sh
or: brew install uv
2. Clone the repo
git clone https://github.com/botcircuits-ai/botcircuits-agent cd botcircuits-agent
3. Pick a Python (3.11+) and create the project venv
uv python install 3.11 uv venv --python 3.11 # creates ./.venv
4. Activate the venv
source .venv/bin/activate # bash / zsh
5. Install dependencies into the venv
uv sync
Configure your provider, model, and API key:
botcircuits setup
The wizard walks you through provider (anthropic / openai / gemini), model, and API key with arrow-key navigation (↑/↓ to move, Enter to select, Esc to keep the current value). Each pick is saved as you go:
provider and model → ~/.botcircuits/settings.json
API key → ~/.botcircuits/.env (ANTHROPIC_API_KEY / OPENAI_API_KEY / GEMINI_API_KEY, file mode 0600)
Re-running botcircuits setup shows your existing values as defaults, and an existing API key gives you a Keep / Replace / Clear choice instead of re-prompting for the secret.
Form What it does
botcircuits setup Full wizard (currently the LLM section)
botcircuits setup llm Just the LLM provider/model/API-key section
botcircuits setup --user Write to ~/.botcircuits/ (default)
botcircuits setup --project Write to ./.botcircuits/settings.json (shared via VCS)
botcircuits setup --local Write to ./.botcircuits/settings.local.json (gitignored personal override)
Prefer to configure by hand? Copy the env template instead:
cp .env.example .env
.env — API key for the provider you want to use (required)
ANTHROPIC_API_KEY=... OPENAI_API_KEY=... GEMINI_API_KEY=...
Optional — only used as a fallback when settings.json / CLI flags don't set them
LLM_PROVIDER=anthropic # anthropic | openai | gemini ANTHROPIC_MODEL=claude-opus-4-7 OPENAI_MODEL=gpt-4.1 GEMINI_MODEL=gemini-2.5-flash
Effective precedence (highest wins): CLI flag → settings.json (layered) → env var → built-in default.
Run
Interactive CLI:
botcircuits botcircuits --provider openai
Pipe a single message (non-interactive):
echo "what is 2+2?" | botcircuits --no-stream
FastAPI gateway (for HTTP + messaging channels):
uv run uvicorn botcircuits.gateway:app --reload --port 8000
or
botcircuits-gateway
Useful CLI flags
Flag Description
--provider anthropic (default) | openai | gemini
--model Override the provider's default model
--stream / --no-stream Force streaming on/off
--auto Skip y/N confirmation on every gated tool. A warning still prints before each action.
--config Load a specific settings.json (in addition to the auto-discovered files)
Settings files (optional)
The CLI auto-loads these in order (later layers win):
Layer Path
User ~/.botcircuits/settings.json
Project (shared) .botcircuits/settings.json
Project (local) .botcircuits/settings.local.json (gitignored)
CLI flags always win over JSON. A starter file is at .botcircuits/settings.example.json.
Slash commands inside the CLI
Command Action
/help Show all commands
/reset Clear the current session and start fresh
/session [id] Show or switch session id
/stream on|off Toggle streaming
/tools List the tools the model can call
/skills List filesystem skills
/memory Show persistent memory
/workflow add "" Author a new workflow from natural language
/workflow edit "" --name Edit an existing workflow
/workflow run --name [--initial-args '{"k":"v"}'] Force-start a workflow tool, bypassing the model's tool choice
/quit Exit
Type """ on its own line to start (and again to end) a multi-line message.
Workflow
A workflow is a step-by-step conversation script the agent can run on your behalf. Each workflow is one JSON file that lives under .botcircuits/workflows/, and once the agent loads it the workflow becomes a tool the model can call by name.
Authoring from the CLI
You don't have to hand-edit JSON. Drive everything through the /workflow slash command:
botcircuits
/workflow add --name workflow_demo "Create a workflow with 11 steps total (step_1 through step_10 plus an end step) that takes one initial argument end_id (a string like 'step_3' or 'step_7' controlling early termination); each numbered step's action is 'Create a markdown file named .md in the current directory containing the current step number and current date and time, e.g., step_3 creates step_3.md with content: Step 3 — '; step_1 through step_9 each have a branching condition: if end_id equals the current step id go to the end step, otherwise go to the next numbered step (step_1 → end if end_id==step_1 else step_2, step_2 → end if end_id==step_2 else step_3, ... through step_9 → end if end_id==step_9 else step_10); step_10 has no condition and goes directly to the end step after its action; the end step's action is 'Create a markdown file named end.md in the current directory containing exactly: END'."
The agent drafts the workflow, shows you a preview, asks y/N, writes the file, and registers it as a tool. The new workflow becomes callable on the very next message — no restart.
By default the model picks a slug for the workflow name. Pass --name to set it yourself — that value becomes both the JSON filename (.botcircuits/workflows/.json) and the registered tool name:
/workflow add "" --name check_order_status
--name must be slug-safe (letters, digits, _, -).
To change an existing workflow:
/workflow edit "also handle refunds" --name check_order_status
The agent reads the current file, applies your edit, asks y/N, and refreshes the live tool.
Force-running a workflow
When you don't want to leave it up to the model to decide whether to invoke a workflow, kick one off directly:
/workflow run --name workflow_demo /workflow run --name workflow_demo --initial-args '{"end_id":"step_3"}'
This calls the workflow tool right away with the args you supplied, seeds the conversation with the resulting first step, and hands control back to the model to perform it. --initial-args must be a JSON object; omit it to start with {}. The target workflow must already be registered — workflow tools are auto-discovered from .botcircuits/workflows/.build/ and the command refreshes that registry before looking up the name, so a freshly authored workflow works without a restart.
Where workflows live
By default, workflows live in .botcircuits/workflows/*.json under the current directory. Override with BOTCIRCUITS_WORKFLOWS_DIR=/abs/path (or set it in .env). A missing directory just means "no workflows" — drop a folder in to opt in.
Each file is one workflow record:
{ "name": "greet_user", "description": "Greet the caller, then say goodbye.", "flow": { "start": "s0", "steps": { "s0": { "type": "start", "next": "a1" }, "a1": { "type": "agentAction", "next": "a2", "conditions": [ { "condition": "the requested tone is warm", "next": "a2" } ], "settings": { "action": "Capture the desired tone and greet the user accordingly." } }, "a2": { "type": "agentAction", "settings": { "action": "Say goodbye." } } } } }
name doubles as the tool name the model calls; it must match ^[a-zA-Z0-9_-]+$. Only start and agentAction step types are supported — to branch, attach a conditions list at the step root (sibling of type and next, not nested inside settings). conditions is control flow, so it sits next to the other control-flow fields rather than with the step-type-specific payload in settings.
Building a workflow
The raw file you author is not what the engine runs. The workflow build step takes the natural-language conditions on each agentAction and prepares conditions and variables the engine can evaluate deterministically:
Conditions — each NL condition (e.g. "the requested tone is warm") is compiled into a typed choices[] entry with an operator (is, >=, contains, …) and a value, so the engine can pick the matching branch without re-calling the LLM at runtime.
Variables — an aggregated flow.variables list is emitted, naming every slot referenced by the compiled conditions along with its inferred dataType and a short description. The runtime uses this list to coerce the LLM's free-text args into the right shape before evaluating branches.
/workflow add|edit runs the builder for you. If you hand-edit a workflow file, re-build it from the CLI:
botcircuits workflow build --name=greet_user
The agent runtime only loads workflows from .botcircuits/workflows/.build/, so a workflow that hasn't been built isn't callable — workflow build is what produces the runnable copy.
Skills
A skill is a small folder of instructions (and optionally allowed tools) the agent reads from disk. Skills are useful for capturing repeatable patterns — "how we answer support questions", "how we draft a PR description" — without baking them into the system prompt.
Where skills live
The CLI discovers skills from:
skills/ (project)
.botcircuits/skills/ (project)
Each skill is a folder with a SKILL.md file:
skills/ └── botcircuits-faq/ └── SKILL.md
--- name: botcircuits-faq description: Answer questions about BotCircuits — features, pricing, docs. allowed-tools: playwrightbrowser_navigate, playwrightbrowser_snapshot ---
You do NOT know about BotCircuits from prior knowledge. The only source of truth is https://botcircuits.ai/. Fetch the site before answering...
The description is what the model uses to decide when to invoke the skill. allowed-tools (optional) restricts which tools the skill is allowed to call during its run.
Listing and running
/skills # list discovered skills / # run a skill directly (bypass the model)
The agent can also pick a skill on its own based on the description.
MCP
MCP servers expose external tools (filesystem, GitHub, databases, …) to the agent. You can configure them once and they become available across every CLI session and gateway request.
Where MCP servers live
MCP server configs live in mcp.json files, layered the same way as settings.json:
Layer Path
User ~/.botcircuits/mcp.json
Project (shared) .botcircuits/mcp.json
Project (local) .botcircuits/mcp.local.json (gitignored)
Two modes:
local — the agent runs the MCP server in-process. Works with every provider, including Gemini.
hosted — the provider executes the MCP server itself (Anthropic and OpenAI only). Gemini auto-promotes hosted entries to local.
Managing from the CLI
List configured servers
botcircuits mcp list
Add a local stdio server (writes to .botcircuits/mcp.json)
botcircuits mcp add fs \ --mode local --transport stdio --command npx \ --args -y @modelcontextprotocol/server-filesystem /tmp
Add a hosted server to your user-wide mcp.json
botcircuits mcp add github --user \ --mode hosted --url https://api.githubcopilot.com/mcp/ \ --authorization-token "$GITHUB_PAT"
Personal override that won't be c
[truncated for AI cost control]