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/completionsservers
- 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+Lor/clear - Scrollable transcript with mouse wheel,
↑/↓, orPgUp/PgDn - Multi-line prompt input with
Shift+Enter; pressEnterto 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+Lto clear the conversation,Ctrl+Dto 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, and/debug on|off, with/helpalso summarizing prompt and transcript keybindings - Automatic project instructions loaded from
AGENTS.mdand local skill files on startup, with/contextand/reload_contextfor inspection and refresh - Persistent custom tools and skills under
~/.pyagent/that survivepip 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 locally from the repo root:
python -m pip install -e .
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 the TUI
After installation, run PyAgent from any directory with:
PyAgent
You can also launch it as a module:
python -m pyagent
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
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.
Model profiles
PyAgent loads named profiles from JSON. By default it looks for:
~/.pyagent/models.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:
ollamaopenai_compatibleopenaivllm
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.
API keys
Profiles can specify either:
api_key— inline secret valueapi_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_PROFILEPYAGENT_PROVIDERPYAGENT_MODELPYAGENT_BASE_URLPYAGENT_API_KEYPYAGENT_API_KEY_ENV
Tool configuration
PYAGENT_TOOLS_ENABLED— enable or disable all model tool calling for the session (trueby default)PYAGENT_BASH_ENABLED— enable or disable thebashtool specifically (trueby 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 (-1means infinite)/cwd— show current working directory/history— show recent prompt history/history search <text>— search saved prompt history for matching prompts/context— show loaded user-global and project instruction files and context size/prompt— show the active system prompt/reload_context— reload~/.pyagent/AGENTS.md,~/.pyagent/skills/**, and local instruction files and report added/removed files/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 promptShift+Enter— insert a newline in the prompt boxCtrl+P/Ctrl+N— move through prompt history↑/↓— scroll the chat transcriptPgUp/PgDn— page through the chat transcriptHome/End— jump to the top or bottom of the chat transcriptCtrl+L— clear the conversationCtrl+D— toggle the debug paneCtrl+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 selectPYAGENT_MODEL_PROFILES_PATH— path to the JSON profile file, overriding the default~/.pyagent/models.jsonlocationPYAGENT_SYSTEM_PROMPT_PATH— path to the system prompt text file, overriding the default~/.pyagent/system_prompt.txtlocationPYAGENT_REQUEST_TIMEOUT— request timeout in secondsPYAGENT_MAX_ITERATIONS— maximum tool loop iterations per user turn (-1means infinite)PYAGENT_MAX_HISTORY_MESSAGES— number of recent non-system messages to keepPYAGENT_STREAM_BATCH_INTERVAL— UI flush interval in secondsPYAGENT_BASH_ENABLED— enable or disable the bash toolPYAGENT_BASH_READONLY_MODE— restrict bash to read-only command prefixesPYAGENT_BASH_TIMEOUT_DEFAULT— default bash timeout in secondsPYAGENT_BASH_BLOCKED_SUBSTRINGS— comma-separated dangerous bash fragments to blockPYAGENT_BASH_READONLY_PREFIXES— comma-separated allowed prefixes in read-only modePYAGENT_USER_DIR— root for user-managed tools, skills, andmodels.json(default~/.pyagent)PYAGENT_USER_TOOLS_ENABLED— discover and register external tools under~/.pyagent/tools/(trueby default)PYAGENT_USER_TOOL_TIMEOUT— wall-clock timeout in seconds for each external tool invocation (default60)PYAGENT_USER_TOOL_DESCRIBE_TIMEOUT— wall-clock timeout for thedescribeschema fetch (default10)PYAGENT_TOOL_RUNNER— executable used to run external tools (defaults touv; advanced override)
Fallback profile env vars when no profile file exists:
PYAGENT_PROVIDERPYAGENT_MODELPYAGENT_BASE_URLPYAGENT_API_KEYPYAGENT_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:
/promptshows 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
/clearto 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/
├── models.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 withname,description,parameters(a JSON-Schema-shaped object), and an optionalversion. By default<runner>isuv. 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>isuv.
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:
mkdir -p ~/.pyagent/tools
cp examples/tools/search_hf_datasets.py ~/.pyagent/tools/
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 reloadre-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
namecollides with a built-in,/toolsshows 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 each source with its scope, and /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
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 pyagent_harness-0.1.1.tar.gz.
File metadata
- Download URL: pyagent_harness-0.1.1.tar.gz
- Upload date:
- Size: 49.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
312e6e11e026e1eb0dcf8f1fb0059e34a5d751a3a63958485301ad8f9a7b2644
|
|
| MD5 |
b00fd97118545661431d7257f1215fb9
|
|
| BLAKE2b-256 |
46ceeb62edca34869335de6293bf35870a8b926dbe6341fb1fb552a39ca38cf1
|
File details
Details for the file pyagent_harness-0.1.1-py3-none-any.whl.
File metadata
- Download URL: pyagent_harness-0.1.1-py3-none-any.whl
- Upload date:
- Size: 48.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
137b689a6091902b0ad5f2f9667e50067a0b79ee0b51ca56ffb2df7734b30b6f
|
|
| MD5 |
306b463bdf6534be1526a8d59213c2fd
|
|
| BLAKE2b-256 |
ce6e0829ddebeefc988ffd0786b2384fe50f4cb01a854cb7cebd55d8e2e8a51b
|