Skip to content

ash-hun/ash-agent

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

5 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ash-agent

An extensible AI agent CLI with a plugin system, MCP client, and multi-provider support.

Python License Docker


Demo

$ ash chat

✦ New session a3f1bc2d (claude-opus-4-7)
plugins: audit_logger
MCP: filesystem (1개 서버)
Type /exit or Ctrl-C to quit.

> list all python files in the current directory
  ⚙ filesystem__list_directory {"path": "."}
  → [file] cli.py  [file] config.py  [dir] agent  [dir] tools ...

claude-opus-4-7: Here are the Python files in the current directory: cli.py, config.py ...
↑980 ↓124  $0.0180

> /exit
Session cost: $0.0180
Bye.

Features

User-facing

  • Plugin system — drop a .py file into ~/.ash-agent/plugins/, get lifecycle hooks and dynamic tool registration with zero configuration
  • MCP client — connect any stdio or Streamable HTTP MCP server by editing mcp.json; tools auto-register as server__tool_name
  • Session persistence — SQLite or PostgreSQL-backed conversation history; resume any past session with --session <id>
  • Full-text searchash search <keyword> searches all past conversations via SQLite FTS5 or PostgreSQL tsvector
  • Cost tracking — real-time token usage and USD cost displayed after every response; session total on exit
  • Cron schedulerash schedule add "..." --cron "0 9 * * *" runs prompts on a schedule; results saved to ~/.ash-agent/schedule/results/
  • Multi-provider — switch between Anthropic (default) and any OpenAI-compatible endpoint (vLLM, etc.) via environment variables
  • TUI — optional React/Ink terminal UI (ash stream or docker compose run tui)

Infrastructure

  • Dual memory backend — SQLite (zero-config, default) or PostgreSQL (team deployments); switch with ASH_MEMORY_BACKEND=postgres
  • Context compression — sliding window compressor keeps the context window under control; configurable via ASH_CONTEXT_MAX_MESSAGES
  • Prompt caching — system prompt and tool definitions are marked with cache_control: ephemeral on every Anthropic request
  • Error classification + provider fallback — rate limit, server error, and timeout automatically retry against a configured fallback provider; auth and context overflow errors surface immediately
  • JSON Lines IPCstream command emits structured events for programmatic consumers (TUI, CI pipelines)

Quick Start

Personal use (pip install)

# Requires Python 3.11+
pip install ash-agent

# Set your API key
export ANTHROPIC_API_KEY=sk-ant-...

# Start chatting
ash chat

CLI commands

ash chat                              # Start a new conversation
ash chat --session <id>               # Resume a previous session
ash sessions                          # List saved sessions
ash search "keyword"                  # Search past conversations
ash schedule add "..." --cron "0 9 * * *"  # Schedule a recurring prompt
ash schedule list                     # View scheduled jobs
ash schedule daemon                   # Start the scheduler daemon

Team deployment (Docker Compose)

For shared team deployments with PostgreSQL, see docs/team-deploy.md.

git clone https://github.com/ash-hun/ash-agents.git
cd ash-agents/ash-agent
cp .env.example .env          # set ANTHROPIC_API_KEY
docker compose build
docker compose run --rm agent

Configuration

All configuration is read from environment variables or a .env file.

Variable Default Description
ANTHROPIC_API_KEY Anthropic API key (required for default provider)
ASH_DEFAULT_PROVIDER anthropic Active provider: anthropic or openai_compat
ASH_DEFAULT_MODEL claude-opus-4-7 Model name passed to the Anthropic provider
VLLM_BASE_URL http://localhost:8000/v1 Base URL for the OpenAI-compatible endpoint
VLLM_API_KEY token-abc123 API key for the OpenAI-compatible endpoint
VLLM_MODEL mistral-7b-instruct Model name for the OpenAI-compatible endpoint
ASH_MAX_ITERATIONS 20 Maximum tool-use iterations per turn
ASH_DB_PATH ~/.ash-agent/sessions.db SQLite database file path
ASH_MEMORY_BACKEND sqlite Memory backend: sqlite or postgres
DATABASE_URL (disabled) PostgreSQL URL — required when ASH_MEMORY_BACKEND=postgres
ASH_CONTEXT_STRATEGY sliding_window Context compression strategy: sliding_window or noop
ASH_CONTEXT_MAX_MESSAGES 40 Maximum messages kept by the sliding window compressor
ASH_PLUGIN_DIR ~/.ash-agent/plugins User-global plugin directory
ASH_FALLBACK_PROVIDER (disabled) Provider to use on rate limit / server error / timeout
ASH_MCP_CONFIG (auto-discover) Explicit path to mcp.json; auto-discovers ./mcp.json then ~/.ash-agent/mcp.json
TAVILY_API_KEY (disabled) Tavily API key — required to enable web_search

Extending with Plugins

Plugins are plain Python files discovered at startup from two locations:

  • ~/.ash-agent/plugins/ — user-global, applies to every project
  • .ash-agent/plugins/ — project-local, relative to the working directory

Files prefixed with _ are ignored. Each file must expose a register(ctx) function.

Step 1 — create the plugin file:

mkdir -p ~/.ash-agent/plugins
touch ~/.ash-agent/plugins/audit_logger.py

Step 2 — implement register(ctx: PluginContext):

# ~/.ash-agent/plugins/audit_logger.py
import logging

log = logging.getLogger("audit")


def register(ctx):
    # Hook: called before every tool execution
    def pre_tool(name: str, input: dict):
        log.info("TOOL %s %s", name, input)

    # Hook: called after every tool execution
    def post_tool(name: str, input: dict, result):
        log.info("RESULT %s -> %s", name, str(result)[:200])

    ctx.on_pre_tool_call(pre_tool)
    ctx.on_post_tool_call(post_tool)

Available hooks:

Hook Signature Fires
on_pre_tool_call (name: str, input: dict) Before each tool execution
on_post_tool_call (name: str, input: dict, result: Any) After each tool execution
on_pre_llm_call (messages: list, tools: list, system: str) Before each LLM call
on_post_llm_call (response: ProviderResponse) After each LLM response

All hooks support both sync and async functions. A hook that raises an exception does not interrupt the agent loop.

Registering a custom tool from a plugin:

def register(ctx):
    async def get_weather(city: str) -> str:
        # ... call your weather API
        return f"Sunny in {city}"

    ctx.register_tool(
        name="get_weather",
        description="Get current weather for a city.",
        schema={
            "type": "object",
            "properties": {"city": {"type": "string"}},
            "required": ["city"],
        },
        fn=get_weather,
    )

The tool is available to the agent immediately — no restart needed beyond the current session startup.


Connecting MCP Servers

Create a mcp.json file in your working directory (or at ~/.ash-agent/mcp.json for user-global config):

{
  "servers": {
    "filesystem": {
      "transport": "stdio",
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"],
      "env": {}
    },
    "git": {
      "transport": "stdio",
      "command": "uvx",
      "args": ["mcp-server-git", "--repository", "."],
      "env": {}
    },
    "my-remote-server": {
      "transport": "sse",
      "url": "http://localhost:3001",
      "headers": {
        "Authorization": "Bearer YOUR_TOKEN"
      }
    }
  }
}

Transport types:

Transport Field Description
stdio command, args, env Spawns a local subprocess; communicates over stdin/stdout JSON-RPC
sse url, headers Connects to a Streamable HTTP MCP server (MCP 2025 spec); POSTs to {url}/mcp

Tool naming convention:

Tools from MCP servers are registered as {server_name}__{tool_name}. For example, a tool named list_directory from a server named filesystem is available to the agent as filesystem__list_directory.

Discovery is automatic — the agent calls tools/list at startup and registers every tool it finds.


Built-in Tools

Tool Description
read_file Read text file contents from a path
write_file Write or overwrite a text file; creates parent directories automatically
list_dir List files and subdirectories at a given path
run_command Execute a shell command and return stdout/stderr; hard timeout of 60 seconds
web_fetch Fetch a URL and return stripped plain text (HTML tags removed, 8000-char cap)
web_search Search the web via Tavily API and return ranked results; requires TAVILY_API_KEY

Architecture

┌─────────────────────────────────────────────────────────────┐
│  CLI  (cli.py)                                              │
│  chat | sessions | stream                                   │
└──────────────────────────┬──────────────────────────────────┘
                           │
           ┌───────────────▼───────────────┐
           │  Agent Loop  (agent/loop.py)  │
           │  while tool_use:              │
           │    pre_llm_hook → LLM call    │
           │    → post_llm_hook            │
           │    pre_tool_hook → dispatch   │
           │    → post_tool_hook           │
           └──┬──────────┬────────┬────────┘
              │          │        │
   ┌──────────▼──┐  ┌────▼────┐  ┌▼──────────────┐
   │  Providers  │  │  Memory    │  │  Tool Registry │
   │  Anthropic  │  │  SQLite    │  │  built-in +    │
   │  OpenAI     │  │  PostgreSQL│  │  MCP tools +   │
   │  Fallback   │  └────────────┘  │  plugin tools  │
   └─────────────┘               └────────────────┘
              │
   ┌──────────▼──────────┐
   │  Context Compressor │
   │  sliding_window     │
   │  noop               │
   └─────────────────────┘

  Plugin system: pre/post hooks fire at each ▲ boundary above
  MCP client: stdio subprocess or Streamable HTTP → tool auto-registration

Key layers:

  • cli.py — entry point; wires up all components and owns the REPL / JSON Lines stream
  • agent/loop.py — stateless agentic loop; accepts any BaseProvider, BaseMemory, and ToolRegistry
  • agent/providers/BaseProvider abstraction; AnthropicProvider, OpenAICompatProvider, FallbackProvider
  • agent/context/ContextCompressor abstraction; SlidingWindowCompressor, NoopCompressor
  • memory/sqlite.pyBaseMemory SQLite implementation; FTS5 full-text search, WAL mode
  • memory/postgres.pyBaseMemory PostgreSQL implementation; tsvector FTS, GIN index
  • tools/registry.py@registry.tool() decorator; sync and async dispatch
  • plugins/__init__.pyPluginManager; file-based discovery, hook emission
  • agent/mcp/MCPManager; transport abstraction (StdioTransport, StreamableHTTPTransport)

Project Structure

ash-agent/
├── cli.py                      # CLI entry point (chat / sessions / stream / search / schedule)
├── config.py                   # Pydantic Settings — all env variables
├── pyproject.toml
├── Dockerfile
├── docker-compose.yml
├── .env.example
├── mcp.json.example
│
├── agent/
│   ├── loop.py                 # Main agent loop
│   ├── prompt.py               # System prompt builder
│   ├── providers/
│   │   ├── base.py             # BaseProvider, ProviderResponse, StopReason
│   │   ├── anthropic_provider.py
│   │   ├── openai_compat.py
│   │   ├── fallback.py         # FallbackProvider wrapper
│   │   └── error.py            # Error classification
│   ├── context/
│   │   ├── base.py             # ContextCompressor interface
│   │   ├── sliding_window.py
│   │   └── noop.py
│   └── mcp/
│       ├── __init__.py         # MCPManager
│       ├── config.py           # MCPConfig, StdioServerConfig, SSEServerConfig
│       └── transport.py        # StdioTransport, StreamableHTTPTransport
│
├── memory/
│   ├── base.py                 # BaseMemory, SessionMeta, MessageRow
│   ├── sqlite.py               # SQLiteMemory (FTS5)
│   └── postgres.py             # PostgresMemory (tsvector + GIN)
│
├── tools/
│   ├── registry.py             # ToolRegistry, @registry.tool()
│   ├── file_tools.py           # read_file, write_file, list_dir
│   ├── shell_tools.py          # run_command
│   ├── web_tools.py            # web_fetch
│   └── search_tools.py         # web_search (Tavily)
│
├── cron/
│   ├── models.py               # ScheduledJob dataclass
│   ├── store.py                # JobStore — SQLite CRUD
│   ├── runner.py               # run_job() — executes a job, saves result file
│   └── daemon.py               # start() — scheduler loop (croniter)
│
├── plugins/                    # (empty — user plugins go in ~/.ash-agent/plugins/)
│
└── ui-tui/                     # Optional React/Ink TUI
    └── src/
        ├── index.tsx
        ├── app.tsx
        ├── agentProcess.ts     # subprocess spawn + JSON Lines IPC
        └── hooks/useAgent.ts

Contributing

  1. Fork the repository and create a feature branch
  2. Install dev dependencies: pip install -e ".[dev]"
  3. For PostgreSQL backend: pip install -e ".[dev,postgres]"
  4. Run the test suite: pytest
  5. Keep commits focused — one concern per commit
  6. Open a pull request with a clear description of the change

New providers, memory backends, and context compression strategies can be added by implementing the corresponding abstract base class (BaseProvider, BaseMemory, ContextCompressor) and wiring it into cli.py.


License

MIT

About

Own my agents

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors