Skip to main content

A lightweight coding agent built with Textual and a configurable multi-provider backend

Project description

PyAgent

A lightweight coding agent built with Textual and a configurable multi-provider chat backend.

Features

  • Streaming chat UI built with Textual
  • Markdown rendering for final assistant and tool messages, with a plain-text fallback for fenced code blocks that contain very long lines so transcript content does not get clipped
  • Tool use for shell commands, file search/text search, file reads/writes/appends/edits, and listing files
  • Optional text-only mode by disabling all model tool calling for a session
  • Provider support for:
    • native Ollama chat endpoints
    • OpenAI-compatible chat endpoints such as OpenAI, vLLM, and other /v1/chat/completions servers
  • OpenAI Python SDK integration for OpenAI-compatible chat completions and model listing
  • Named model profiles stored in JSON for easy switching between endpoints and models
  • API key support through inline values or environment-variable references
  • Conversation reset with Ctrl+L or /clear
  • Scrollable transcript with mouse wheel, / , or PgUp / PgDn
  • Multi-line prompt input with Shift+Enter; press Enter to send, the input box auto-grows as you type, and the prompt area shows a helper hint
  • Prompt history with Ctrl+P / Ctrl+N, plus /history search <text> from the TUI
  • Keyboard shortcuts including Ctrl+L to clear the conversation, Ctrl+D to toggle the debug pane, and transcript scrolling with / / PgUp / PgDn / Home / End
  • Slash commands such as /help, /tools, /profiles, /profile, /model, /status, /cwd, /history, /context, /prompt, /reload_context, /logging, and /debug on|off, with /help also summarizing prompt and transcript keybindings
  • Automatic layered instructions loaded from user-global ~/.pyagent/AGENTS.md and ~/.pyagent/skills/**, plus project-local AGENTS.md and skill files on startup, with /context and /reload_context for inspection and refresh
  • Persistent custom tools and skills under ~/.pyagent/ that survive pip install --upgrade. Each user-managed tool is a standalone UV script (PEP 723) with click subcommands, so adding a new tool with new dependencies never touches the core install

Requirements

  • Python 3.10+
  • A supported endpoint such as:
    • Ollama
    • OpenAI
    • vLLM or another OpenAI-compatible server
  • A model with tool-calling support

Installation

Install PyAgent via pip:

pip install pyagent-harness

Install PyAgent with API support:

pip install pyagent-harness[api]

If you are developing and want to install locally from the repo root:

python -m pip install -e .

If you want the local editable install with API support:

python -m pip install -e '.[api]'

If you want a non-editable local install from the current directory:

python -m pip install .

And with API support from the current directory:

python -m pip install '.[api]'

If you only want the dependencies without installing the package entry point, this still works:

pip install -r requirements.txt

PyAgent uses the openai Python SDK for OpenAI-compatible profiles and keeps the native Ollama HTTP path for Ollama profiles.

Running PyAgent

Interactive Mode (TUI)

After installation, run PyAgent from any directory with:

pyagent

You can also launch it as a module:

python -m pyagent

Single-Shot Mode (CLI)

You can run a single prompt and exit directly from the command line:

pyagent --prompt "What files are in the current directory?"

Single-shot mode automatically loads layered instruction context just like startup in the TUI:

  • ~/.pyagent/AGENTS.md
  • project-local AGENTS.md
  • project-local *.skill
  • project-local skills/**/*.md
  • project-local skills/**/*.skill

You can also load specific user skills from ~/.pyagent/skills/ with --skills by passing comma-separated paths relative to that directory:

pyagent --skills code-review.md,python/testing.skill --prompt "Review this repository's testing strategy"

If any listed skill does not exist under ~/.pyagent/skills/, PyAgent exits with an error. The --skills flag is currently supported only together with --prompt.

Managing user skills and tools

PyAgent can install, list, and remove user-managed skills and external tools under ~/.pyagent/ from the command line.

pyagent skills list
pyagent skills install ./review.md
pyagent skills install https://example.com/review.md --name review.md
pyagent skills remove review.md

pyagent tools list
pyagent tools install ./my_tool.py
pyagent tools install https://example.com/my_tool.py --name my_tool.py
pyagent tools remove my_tool.py

Notes:

  • Skills are installed under ~/.pyagent/skills/ and must use .md or .skill.
  • Tools are installed under ~/.pyagent/tools/ and must use .py.
  • Use --force with install to overwrite an existing file.
  • Installed tools are marked executable automatically, because apparently software enjoys tiny rituals.

Options

To choose a saved profile and optionally override its model for the current session:

pyagent --profile local-qwen
pyagent --profile openai-gpt4 --model gpt-4.1-mini

You can combine these with --prompt, and optionally --skills, for one-shot runs:

pyagent --profile openai-gpt4 --prompt "Summarize the contents of README.md"
pyagent --profile openai-gpt4 --model gpt-4.1-mini --skills repo/review.md --prompt "Review the current project instructions"

If the current working directory contains AGENTS.md, *.skill, or files under skills/**/*.md / skills/**/*.skill, PyAgent will load them into the system prompt automatically at startup. You can inspect the currently loaded sources with /context and refresh them while the app is running with /reload_context.

HTTP API

PyAgent also includes an optional FastAPI server so you can use the same agent loop over HTTP instead of the TUI.

Install the extra runtime pieces first:

pip install pyagent-harness[api]

If you are running from a local checkout instead of installing from an index:

python -m pip install '.[api]'

Then start the API server:

pyagent serve

Optional bind overrides:

pyagent serve --host 0.0.0.0 --port 8000

Browser UI

If you want to access the Textual app from a web browser instead of a local terminal, PyAgent also exposes a web subcommand powered by textual-serve:

pyagent web

Optional bind and model/profile overrides:

pyagent web --host 0.0.0.0 --port 8000
pyagent web --profile local-qwen --model qwen2.5-coder:7b

This launches a small server that serves the Textual interface in the browser while still running the normal python -m pyagent app underneath.

Endpoints:

  • GET /health — basic health check
  • POST /run — run a single non-streaming agent turn

Example request:

curl -X POST http://127.0.0.1:8000/run \
  -H 'Content-Type: application/json' \
  -d '{
    "message": "Summarize README.md",
    "messages": [
      {"role": "user", "content": "We already discussed installation."}
    ],
    "profile": "local-qwen",
    "model": "qwen2.5-coder:7b",
    "cwd": ".",
    "skills": ["code-review.md"]
  }'

Example response:

{
  "response": "...",
  "profile": "local-qwen",
  "provider": "ollama",
  "model": "qwen2.5-coder:7b",
  "context_files": ["~/.pyagent/AGENTS.md", "AGENTS.md"]
}

Python client

PyAgent also ships with a small synchronous HTTP client for the API. It uses the Python standard library, so it does not require the server-side api extras just to make requests.

from pyagent.client import PyAgentClient

client = PyAgentClient("http://127.0.0.1:8000")

print(client.health())

result = client.run(
    "Summarize README.md",
    messages=[{"role": "user", "content": "We already discussed installation."}],
    profile="local-qwen",
    model="qwen2.5-coder:7b",
    cwd=".",
    skills=["code-review.md"],
)

print(result.response)
print(result.profile, result.provider, result.model)
print(result.context_files)

Client details:

  • PyAgentClient.health() returns the decoded /health JSON payload.
  • PyAgentClient.is_healthy() returns True/False without raising on connection failures.
  • PyAgentClient.run(...) returns a typed RunResponse object.
  • PyAgentClientError is raised for HTTP errors, invalid JSON responses, connection failures, and timeouts.
  • The default base URL is http://127.0.0.1:8000.

The API uses the same profile selection, model override, context loading, and optional user skill validation as single-shot CLI mode. You may also pass prior conversation history in the optional messages field on POST /run; PyAgent preserves its own active system prompt and ignores any incoming system messages so runtime instructions cannot be overridden by API callers. If FastAPI or Uvicorn are not installed, pyagent api exits with a clear error.

Model profiles

PyAgent loads named profiles from JSON. By default it looks for:

~/.pyagent/profiles.json

You can override the location with:

  • PYAGENT_MODEL_PROFILES_PATH

A sample file is included in the repo as models.example.json.

Example profile file

{
  "default_profile": "local-qwen",
  "profiles": {
    "local-qwen": {
      "provider": "ollama",
      "base_url": "http://localhost:11434",
      "model": "qwen2.5-coder:7b"
    },
    "openai-gpt4": {
      "provider": "openai_compatible",
      "base_url": "https://api.openai.com/v1",
      "model": "gpt-4.1",
      "api_key_env": "OPENAI_API_KEY"
    },
    "vllm-local": {
      "provider": "vllm",
      "base_url": "http://localhost:8000/v1",
      "model": "Qwen/Qwen2.5-Coder-32B-Instruct",
      "api_key_env": "VLLM_API_KEY"
    }
  }
}

Provider values:

  • ollama
  • openai_compatible
  • openai
  • vllm

openai and vllm are treated as OpenAI-compatible providers.

OpenAI-compatible profiles use the openai Python SDK with the Chat Completions API. This keeps PyAgent on /v1/chat/completions rather than the newer Responses API so it remains compatible with OpenAI-style servers such as OpenAI and vLLM.

OpenAI-compatible transport overrides

OpenAI-compatible profiles may optionally include httpx_kwargs to customize the underlying httpx.Client used by the OpenAI SDK. This is mainly useful for local or self-hosted endpoints that need non-default transport settings.

Example:

{
  "default_profile": "local-vllm",
  "profiles": {
    "local-vllm": {
      "provider": "vllm",
      "base_url": "https://localhost:8000/v1",
      "model": "Qwen/Qwen2.5-Coder-32B-Instruct",
      "httpx_kwargs": {
        "verify": false
      }
    }
  }
}

Notes:

  • httpx_kwargs is only used for OpenAI-compatible providers.
  • If httpx_kwargs is omitted, PyAgent uses the SDK's default HTTP client configuration.
  • For backwards compatibility, the older field name http_kwargs is still accepted when loading profiles, but httpx_kwargs is preferred.
  • If both httpx_kwargs and http_kwargs are present, httpx_kwargs wins.

API keys

Profiles can specify either:

  • api_key — inline secret value
  • api_key_env — environment variable name to read at runtime

Using api_key_env is recommended.

For local OpenAI-compatible servers that do not require authentication, you can omit both api_key and api_key_env.

Fallback behavior

If the profile file does not exist, PyAgent creates an implicit default profile from environment variables.

Useful env vars for that fallback:

  • PYAGENT_PROFILE
  • PYAGENT_PROVIDER
  • PYAGENT_MODEL
  • PYAGENT_BASE_URL
  • PYAGENT_API_KEY
  • PYAGENT_API_KEY_ENV

Tool configuration

  • PYAGENT_TOOLS_ENABLED — enable or disable all model tool calling for the session (true by default)
  • PYAGENT_BASH_ENABLED — enable or disable the bash tool specifically (true by default)

When PYAGENT_TOOLS_ENABLED=false, PyAgent does not advertise tools to the model and adds a system instruction telling it not to call tools.

Runtime slash commands

  • /tools — show current tool status, built-in tools, external user tools, and any broken/disabled scripts
  • /tools on — enable model tool calling for the current session
  • /tools off — disable model tool calling for the current session
  • /tools reload — re-scan ~/.pyagent/tools/ and rebuild the tool registry (also available as /reload_tools)
  • /tools new <name> — scaffold a starter UV-script tool at ~/.pyagent/tools/<name>.py
  • /tools enable <name> — move a script out of ~/.pyagent/tools/disabled/
  • /tools disable <name> — move a script into ~/.pyagent/tools/disabled/
  • /tools open <name> — print the absolute path to a tool script

Changing tool mode at runtime resets the current conversation so the updated system prompt is applied cleanly.

  • /clear — clear the conversation
  • /help — show command help
  • /tools — list tools
  • /profiles — list saved profiles, including current/default markers and auth hints
  • /profiles reload — reload profiles from disk
  • /reload_profiles — reload profiles from disk
  • /profile — show the active profile
  • /profile <name> — switch to a saved profile
  • /profile add <name> provider=<provider> model=<model> [base_url=<url>] [api_key_env=<ENV>] [api_key=<KEY>] [default=true|false] [switch=true|false] [header.<Name>=<Value>] — create or update a profile from the TUI
  • /model — show the active model
  • /model list — ask the current endpoint for available models, if supported
  • /model <name> — override the current profile's model for this session
  • /status — show current configuration, including the agent tool-loop max-iteration setting
  • /max_iterations <n|-1> — set the maximum tool-loop iterations for the current session (-1 means infinite)
  • /cwd — show current working directory
  • /history — show recent prompt history
  • /history search <text> — search saved prompt history for matching prompts
  • /context — show loaded instruction sources and context size, including user-global defaults, auto-loaded global skills, session-loaded user skills, and project-local files
  • /prompt — show the active system prompt
  • /reload_context — reload ~/.pyagent/AGENTS.md, ~/.pyagent/skills/**, and local instruction files and report added/removed files
  • /logging on|off — enable or disable session logging (saved to ~/.pyagent/logs/)
  • /debug — show whether the debug pane is currently on or off
  • /debug on|off — show or hide the debug pane

Unknown slash commands may suggest a close match, for example /stats may suggest /status.

Keyboard shortcuts

  • Enter — send the current prompt
  • Shift+Enter — insert a newline in the prompt box
  • Ctrl+P / Ctrl+N — move through prompt history
  • / — scroll the chat transcript
  • PgUp / PgDn — page through the chat transcript
  • Home / End — jump to the top or bottom of the chat transcript
  • Ctrl+L — clear the conversation
  • Ctrl+D — toggle the debug pane
  • Ctrl+C — quit the app

Profile creation from the TUI

Profile creation and updates are available through /profile add. Values containing spaces should be quoted.

Examples:

/profile add local-14b provider=ollama model=qwen2.5-coder:14b switch=true
/profile add openai-mini provider=openai model=gpt-4.1-mini api_key_env=OPENAI_API_KEY default=true
/profile add vllm-qwen provider=vllm model="Qwen/Qwen2.5-Coder-32B-Instruct" base_url=http://localhost:8000/v1 api_key_env=VLLM_API_KEY header.X-Project=PyAgent

Configuration

Environment variables:

  • PYAGENT_PROFILE — default profile name to select
  • PYAGENT_MODEL_PROFILES_PATH — path to the JSON profile file, overriding the default ~/.pyagent/profiles.json location
  • PYAGENT_SYSTEM_PROMPT_PATH — path to the system prompt text file, overriding the default ~/.pyagent/system_prompt.txt location
  • PYAGENT_REQUEST_TIMEOUT — request timeout in seconds
  • PYAGENT_MAX_ITERATIONS — maximum tool loop iterations per user turn (-1 means infinite)
  • PYAGENT_MAX_HISTORY_MESSAGES — number of recent non-system messages to keep
  • PYAGENT_STREAM_BATCH_INTERVAL — UI flush interval in seconds
  • PYAGENT_BASH_ENABLED — enable or disable the bash tool
  • PYAGENT_BASH_READONLY_MODE — restrict bash to read-only command prefixes
  • PYAGENT_BASH_TIMEOUT_DEFAULT — default bash timeout in seconds
  • PYAGENT_BASH_BLOCKED_SUBSTRINGS — comma-separated dangerous bash fragments to block
  • PYAGENT_BASH_READONLY_PREFIXES — comma-separated allowed prefixes in read-only mode
  • PYAGENT_USER_DIR — root for user-managed tools, skills, and profiles.json (default ~/.pyagent)
  • PYAGENT_USER_TOOLS_ENABLED — discover and register external tools under ~/.pyagent/tools/ (true by default)
  • PYAGENT_USER_TOOL_TIMEOUT — wall-clock timeout in seconds for each external tool invocation (default 60)
  • PYAGENT_USER_TOOL_DESCRIBE_TIMEOUT — wall-clock timeout for the describe schema fetch (default 10)
  • PYAGENT_TOOL_RUNNER — executable used to run external tools (defaults to uv; advanced override)

Profile JSON fields:

  • headers — optional object of extra HTTP headers to send with a profile
  • httpx_kwargs — optional object of keyword arguments passed to httpx.Client for OpenAI-compatible profiles only
  • http_kwargs — legacy alias for httpx_kwargs, still accepted for compatibility but not recommended for new configs

Fallback profile env vars when no profile file exists:

  • PYAGENT_PROVIDER
  • PYAGENT_MODEL
  • PYAGENT_BASE_URL
  • PYAGENT_API_KEY
  • PYAGENT_API_KEY_ENV

Custom system prompt

PyAgent stores the active system prompt in a text file. By default that file is:

~/.pyagent/system_prompt.txt

On first run, PyAgent creates that file automatically if it does not already exist.

You can override the location with:

  • PYAGENT_SYSTEM_PROMPT_PATH

Examples:

export PYAGENT_SYSTEM_PROMPT_PATH="$HOME/.config/pyagent/my_prompt.txt"
pyagent

Or edit the default prompt file directly:

mkdir -p ~/.pyagent
$EDITOR ~/.pyagent/system_prompt.txt

A few useful notes:

  • /prompt shows the currently active system prompt inside the TUI.
  • The system prompt is loaded when the conversation is initialized or reset, so after editing the file you should use /clear to start a fresh conversation with the updated prompt.
  • Project and user instruction files (AGENTS.md, skills/**, *.skill) are layered onto the base system prompt automatically.

Custom tools and skills

Anything you add for yourself — custom tools, custom skills, custom AGENTS.md instructions — should live under ~/.pyagent/ so a pip install --upgrade of PyAgent does not wipe it out. Built-in tools (bash, list_files, find_files, search_text, read_file, write_file, append_file, edit_file) stay inside the package; user tools layer on top.

Layout

~/.pyagent/
├── profiles.json                      # named model profiles (existing)
├── AGENTS.md                        # optional user-global agent instructions
├── skills/                          # user-global skills (*.md, *.skill)
└── tools/                           # user tools (one UV script per tool)
    ├── <my_tool>.py
    ├── disabled/                    # listed in /tools but not registered
    └── .cache/manifests.json        # auto schema cache (path+mtime+size keyed)

Custom tools (UV scripts with click subcommands)

Each user tool is a single self-contained Python file. PyAgent runs it through uv so its dependencies are declared inline (PEP 723) and installed into an isolated venv on first invocation. The core PyAgent install never grows when you add a new tool.

Every tool must implement two CLI subcommands:

  • <runner> run <script> describe — print a JSON manifest with name, description, parameters (a JSON-Schema-shaped object), and an optional version. By default <runner> is uv. The output is cached by path + mtime + size, so subsequent startups skip the subprocess.
  • <runner> run <script> invoke --args-file <path> — read the tool arguments as a JSON object from <path>, print the result to stdout, and exit non-zero with an error on stderr if anything goes wrong. By default <runner> is uv.

Use /tools new <name> from inside PyAgent to scaffold a starter file, or write one by hand. The built-in scaffold and examples use uv, which is the recommended runner. Skeleton:

#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.10"
# dependencies = ["click", "huggingface_hub", "datasets"]
# ///
import json
import sys
from pathlib import Path
import click


@click.group()
def cli():
    pass


@cli.command()
def describe():
    click.echo(json.dumps({
        "name": "my_tool",
        "description": "What this tool does — sent verbatim to the model.",
        "parameters": {
            "type": "object",
            "properties": {"input": {"type": "string"}},
            "required": ["input"],
        },
        "version": "1",
    }))


@cli.command()
@click.option("--args-file", required=True, type=click.Path(exists=True, path_type=Path))
def invoke(args_file):
    args = json.loads(args_file.read_text())
    click.echo(my_logic(**args))


if __name__ == "__main__":
    cli()

Reference example: search_hf_datasets

examples/tools/search_hf_datasets.py is a fully fleshed-out reference tool (Hugging Face dataset search) using the same contract. To install it for yourself:

pyagent tools install examples/tools/search_hf_datasets.py

Then inside PyAgent run /tools reload. UV will install huggingface_hub and datasets on first invocation, into the script's own venv — your PyAgent install stays lean.

Lifecycle

  • New / changed scripts: /tools reload re-scans the directory and rebuilds the registry. The schema cache invalidates automatically when the file's path, mtime, or size changes.
  • Temporarily turn a tool off: /tools disable <name> moves it to ~/.pyagent/tools/disabled/ (still listed in /tools, not registered).
  • Re-enable: /tools enable <name>.
  • Locate a script: /tools open <name> prints the absolute path.
  • Name collisions: built-ins always win. If your script's name collides with a built-in, /tools shows a warning row with the colliding script path so you can rename it.
  • Bad scripts (timeout, non-zero describe, malformed JSON) are listed under "Broken external tools" and skipped; healthy tools keep loading.
  • Missing uv: external tools are disabled at startup with a clear banner; built-ins continue to work.

Custom skills and AGENTS.md

~/.pyagent/AGENTS.md, ~/.pyagent/skills/**/*.md, and ~/.pyagent/skills/**/*.skill are loaded into the system prompt at startup as user-global instructions, layered before any project-specific AGENTS.md or skills/ files in the current working directory. /context lists these sources separately so you can distinguish user-global defaults, auto-loaded global skills, session-loaded user skills, and project-local files. /reload_context re-scans both layers.

Trust boundary

~/.pyagent/tools/ is user-owned. PyAgent enforces wall-clock timeouts (PYAGENT_USER_TOOL_TIMEOUT, PYAGENT_USER_TOOL_DESCRIBE_TIMEOUT) but does not otherwise sandbox these scripts. Treat any tool you drop into ~/.pyagent/tools/ the same as you would any code you choose to run.

Quick CLI smoke test

python test_agent.py

Development test commands

For non-trivial changes, run:

python -m py_compile pyagent/*.py test_agent.py
python -m unittest -v

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pyagent_harness-0.7.1.tar.gz (68.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pyagent_harness-0.7.1-py3-none-any.whl (67.8 kB view details)

Uploaded Python 3

File details

Details for the file pyagent_harness-0.7.1.tar.gz.

File metadata

  • Download URL: pyagent_harness-0.7.1.tar.gz
  • Upload date:
  • Size: 68.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.12

File hashes

Hashes for pyagent_harness-0.7.1.tar.gz
Algorithm Hash digest
SHA256 22681c28a2afcff0d27c96d06bf1531ffce22302c5655673d551bb4dfa15e001
MD5 c8070dc02ffcd1a1945c40fbdfb829ef
BLAKE2b-256 87dd6af3a9f86222e793202090433fbe769126848a293bbf76c71100dae8cd00

See more details on using hashes here.

File details

Details for the file pyagent_harness-0.7.1-py3-none-any.whl.

File metadata

File hashes

Hashes for pyagent_harness-0.7.1-py3-none-any.whl
Algorithm Hash digest
SHA256 106084e5e7067c3ccce220382683ecea99618fac68e8a0f1c444d4ec1d3cb435
MD5 22fa0f640193dba0fecfd2c4f7da7ac2
BLAKE2b-256 146fb38c2701c2d967aa2a88e7320a215ae1e6cf6d282badc557473f664e2f9e

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page