pyagent-harness 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jacob Renn
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,408 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyagent-harness
3
+ Version: 0.1.0
4
+ Summary: A lightweight coding agent built with Textual and a configurable multi-provider backend
5
+ Requires-Python: >=3.10
6
+ Description-Content-Type: text/markdown
7
+ License-File: LICENSE
8
+ Requires-Dist: textual
9
+ Requires-Dist: requests
10
+ Requires-Dist: openai>=2.14.0
11
+ Dynamic: license-file
12
+
13
+ # PyAgent
14
+
15
+ A lightweight coding agent built with Textual and a configurable multi-provider chat backend.
16
+
17
+ ## Features
18
+
19
+ - **Streaming chat UI** built with Textual
20
+ - **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
21
+ - **Tool use** for shell commands, file search/text search, file reads/writes/appends/edits, and listing files
22
+ - **Optional text-only mode** by disabling all model tool calling for a session
23
+ - **Provider support** for:
24
+ - native **Ollama** chat endpoints
25
+ - **OpenAI-compatible** chat endpoints such as OpenAI, vLLM, and other `/v1/chat/completions` servers
26
+ - **OpenAI Python SDK integration** for OpenAI-compatible chat completions and model listing
27
+ - **Named model profiles** stored in JSON for easy switching between endpoints and models
28
+ - **API key support** through inline values or environment-variable references
29
+ - **Conversation reset** with `Ctrl+L` or `/clear`
30
+ - **Scrollable transcript** with mouse wheel, `↑` / `↓`, or `PgUp` / `PgDn`
31
+ - **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
32
+ - **Prompt history** with `Ctrl+P` / `Ctrl+N`, plus `/history search <text>` from the TUI
33
+ - **Keyboard shortcuts** including `Ctrl+L` to clear the conversation, `Ctrl+D` to toggle the debug pane, and transcript scrolling with `↑` / `↓` / `PgUp` / `PgDn` / `Home` / `End`
34
+ - **Slash commands** such as `/help`, `/tools`, `/profiles`, `/profile`, `/model`, `/status`, `/cwd`, `/history`, `/context`, `/prompt`, `/reload_context`, and `/debug on|off`, with `/help` also summarizing prompt and transcript keybindings
35
+ - **Automatic project instructions** loaded from `AGENTS.md` and local skill files on startup, with `/context` and `/reload_context` for inspection and refresh
36
+ - **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
37
+
38
+ ## Requirements
39
+
40
+ - Python 3.10+
41
+ - A supported endpoint such as:
42
+ - [Ollama](https://ollama.com)
43
+ - OpenAI
44
+ - vLLM or another OpenAI-compatible server
45
+ - A model with tool-calling support
46
+
47
+ ## Installation
48
+
49
+ Install PyAgent locally from the repo root:
50
+
51
+ ```bash
52
+ python -m pip install -e .
53
+ ```
54
+
55
+ If you only want the dependencies without installing the package entry point, this still works:
56
+
57
+ ```bash
58
+ pip install -r requirements.txt
59
+ ```
60
+
61
+ PyAgent uses the `openai` Python SDK for OpenAI-compatible profiles and keeps the native Ollama HTTP path for Ollama profiles.
62
+
63
+ ## Running the TUI
64
+
65
+ After installation, run PyAgent from any directory with:
66
+
67
+ ```bash
68
+ PyAgent
69
+ ```
70
+
71
+ You can also launch it as a module:
72
+
73
+ ```bash
74
+ python -m pyagent
75
+ ```
76
+
77
+ To choose a saved profile and optionally override its model for the current session:
78
+
79
+ ```bash
80
+ PyAgent --profile local-qwen
81
+ PyAgent --profile openai-gpt4 --model gpt-4.1-mini
82
+ ```
83
+
84
+ 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`.
85
+
86
+ ## Model profiles
87
+
88
+ PyAgent loads named profiles from JSON. By default it looks for:
89
+
90
+ ```text
91
+ ~/.pyagent/models.json
92
+ ```
93
+
94
+ You can override the location with:
95
+
96
+ - `PYAGENT_MODEL_PROFILES_PATH`
97
+
98
+ A sample file is included in the repo as `models.example.json`.
99
+
100
+ ### Example profile file
101
+
102
+ ```json
103
+ {
104
+ "default_profile": "local-qwen",
105
+ "profiles": {
106
+ "local-qwen": {
107
+ "provider": "ollama",
108
+ "base_url": "http://localhost:11434",
109
+ "model": "qwen2.5-coder:7b"
110
+ },
111
+ "openai-gpt4": {
112
+ "provider": "openai_compatible",
113
+ "base_url": "https://api.openai.com/v1",
114
+ "model": "gpt-4.1",
115
+ "api_key_env": "OPENAI_API_KEY"
116
+ },
117
+ "vllm-local": {
118
+ "provider": "vllm",
119
+ "base_url": "http://localhost:8000/v1",
120
+ "model": "Qwen/Qwen2.5-Coder-32B-Instruct",
121
+ "api_key_env": "VLLM_API_KEY"
122
+ }
123
+ }
124
+ }
125
+ ```
126
+
127
+ Provider values:
128
+
129
+ - `ollama`
130
+ - `openai_compatible`
131
+ - `openai`
132
+ - `vllm`
133
+
134
+ `openai` and `vllm` are treated as OpenAI-compatible providers.
135
+
136
+ 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.
137
+
138
+ ### API keys
139
+
140
+ Profiles can specify either:
141
+
142
+ - `api_key` — inline secret value
143
+ - `api_key_env` — environment variable name to read at runtime
144
+
145
+ Using `api_key_env` is recommended.
146
+
147
+ For local OpenAI-compatible servers that do not require authentication, you can omit both `api_key` and `api_key_env`.
148
+
149
+ ### Fallback behavior
150
+
151
+ If the profile file does not exist, PyAgent creates an implicit `default` profile from environment variables.
152
+
153
+ Useful env vars for that fallback:
154
+
155
+ - `PYAGENT_PROFILE`
156
+ - `PYAGENT_PROVIDER`
157
+ - `PYAGENT_MODEL`
158
+ - `PYAGENT_BASE_URL`
159
+ - `PYAGENT_API_KEY`
160
+ - `PYAGENT_API_KEY_ENV`
161
+
162
+ ## Tool configuration
163
+
164
+ - `PYAGENT_TOOLS_ENABLED` — enable or disable all model tool calling for the session (`true` by default)
165
+ - `PYAGENT_BASH_ENABLED` — enable or disable the `bash` tool specifically (`true` by default)
166
+
167
+ When `PYAGENT_TOOLS_ENABLED=false`, PyAgent does not advertise tools to the model and adds a system instruction telling it not to call tools.
168
+
169
+ ## Runtime slash commands
170
+
171
+ - `/tools` — show current tool status, built-in tools, external user tools, and any broken/disabled scripts
172
+ - `/tools on` — enable model tool calling for the current session
173
+ - `/tools off` — disable model tool calling for the current session
174
+ - `/tools reload` — re-scan `~/.pyagent/tools/` and rebuild the tool registry (also available as `/reload_tools`)
175
+ - `/tools new <name>` — scaffold a starter UV-script tool at `~/.pyagent/tools/<name>.py`
176
+ - `/tools enable <name>` — move a script out of `~/.pyagent/tools/disabled/`
177
+ - `/tools disable <name>` — move a script into `~/.pyagent/tools/disabled/`
178
+ - `/tools open <name>` — print the absolute path to a tool script
179
+
180
+ Changing tool mode at runtime resets the current conversation so the updated system prompt is applied cleanly.
181
+
182
+ - `/clear` — clear the conversation
183
+ - `/help` — show command help
184
+ - `/tools` — list tools
185
+ - `/profiles` — list saved profiles, including current/default markers and auth hints
186
+ - `/profiles reload` — reload profiles from disk
187
+ - `/reload_profiles` — reload profiles from disk
188
+ - `/profile` — show the active profile
189
+ - `/profile <name>` — switch to a saved profile
190
+ - `/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
191
+ - `/model` — show the active model
192
+ - `/model list` — ask the current endpoint for available models, if supported
193
+ - `/model <name>` — override the current profile's model for this session
194
+ - `/status` — show current configuration, including the agent tool-loop max-iteration setting
195
+ - `/max_iterations <n|-1>` — set the maximum tool-loop iterations for the current session (`-1` means infinite)
196
+ - `/cwd` — show current working directory
197
+ - `/history` — show recent prompt history
198
+ - `/history search <text>` — search saved prompt history for matching prompts
199
+ - `/context` — show loaded user-global and project instruction files and context size
200
+ - `/prompt` — show the active system prompt
201
+ - `/reload_context` — reload `~/.pyagent/AGENTS.md`, `~/.pyagent/skills/**`, and local instruction files and report added/removed files
202
+ - `/debug` — show whether the debug pane is currently on or off
203
+ - `/debug on|off` — show or hide the debug pane
204
+
205
+ Unknown slash commands may suggest a close match, for example `/stats` may suggest `/status`.
206
+
207
+ ## Keyboard shortcuts
208
+
209
+ - `Enter` — send the current prompt
210
+ - `Shift+Enter` — insert a newline in the prompt box
211
+ - `Ctrl+P` / `Ctrl+N` — move through prompt history
212
+ - `↑` / `↓` — scroll the chat transcript
213
+ - `PgUp` / `PgDn` — page through the chat transcript
214
+ - `Home` / `End` — jump to the top or bottom of the chat transcript
215
+ - `Ctrl+L` — clear the conversation
216
+ - `Ctrl+D` — toggle the debug pane
217
+ - `Ctrl+C` — quit the app
218
+
219
+ ### Profile creation from the TUI
220
+
221
+ Profile creation and updates are available through `/profile add`.
222
+ Values containing spaces should be quoted.
223
+
224
+ Examples:
225
+
226
+ ```text
227
+ /profile add local-14b provider=ollama model=qwen2.5-coder:14b switch=true
228
+ /profile add openai-mini provider=openai model=gpt-4.1-mini api_key_env=OPENAI_API_KEY default=true
229
+ /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
230
+ ```
231
+
232
+ ## Configuration
233
+
234
+ Environment variables:
235
+
236
+ - `PYAGENT_PROFILE` — default profile name to select
237
+ - `PYAGENT_MODEL_PROFILES_PATH` — path to the JSON profile file, overriding the default `~/.pyagent/models.json` location
238
+ - `PYAGENT_SYSTEM_PROMPT_PATH` — path to the system prompt text file, overriding the default `~/.pyagent/system_prompt.txt` location
239
+ - `PYAGENT_REQUEST_TIMEOUT` — request timeout in seconds
240
+ - `PYAGENT_MAX_ITERATIONS` — maximum tool loop iterations per user turn (`-1` means infinite)
241
+ - `PYAGENT_MAX_HISTORY_MESSAGES` — number of recent non-system messages to keep
242
+ - `PYAGENT_STREAM_BATCH_INTERVAL` — UI flush interval in seconds
243
+ - `PYAGENT_BASH_ENABLED` — enable or disable the bash tool
244
+ - `PYAGENT_BASH_READONLY_MODE` — restrict bash to read-only command prefixes
245
+ - `PYAGENT_BASH_TIMEOUT_DEFAULT` — default bash timeout in seconds
246
+ - `PYAGENT_BASH_BLOCKED_SUBSTRINGS` — comma-separated dangerous bash fragments to block
247
+ - `PYAGENT_BASH_READONLY_PREFIXES` — comma-separated allowed prefixes in read-only mode
248
+ - `PYAGENT_USER_DIR` — root for user-managed tools, skills, and `models.json` (default `~/.pyagent`)
249
+ - `PYAGENT_USER_TOOLS_ENABLED` — discover and register external tools under `~/.pyagent/tools/` (`true` by default)
250
+ - `PYAGENT_USER_TOOL_TIMEOUT` — wall-clock timeout in seconds for each external tool invocation (default `60`)
251
+ - `PYAGENT_USER_TOOL_DESCRIBE_TIMEOUT` — wall-clock timeout for the `describe` schema fetch (default `10`)
252
+ - `PYAGENT_TOOL_RUNNER` — executable used to run external tools (defaults to `uv`; advanced override)
253
+
254
+ Fallback profile env vars when no profile file exists:
255
+
256
+ - `PYAGENT_PROVIDER`
257
+ - `PYAGENT_MODEL`
258
+ - `PYAGENT_BASE_URL`
259
+ - `PYAGENT_API_KEY`
260
+ - `PYAGENT_API_KEY_ENV`
261
+
262
+ ## Custom system prompt
263
+
264
+ PyAgent stores the active system prompt in a text file. By default that file is:
265
+
266
+ ```text
267
+ ~/.pyagent/system_prompt.txt
268
+ ```
269
+
270
+ On first run, PyAgent creates that file automatically if it does not already exist.
271
+
272
+ You can override the location with:
273
+
274
+ - `PYAGENT_SYSTEM_PROMPT_PATH`
275
+
276
+ Examples:
277
+
278
+ ```bash
279
+ export PYAGENT_SYSTEM_PROMPT_PATH="$HOME/.config/pyagent/my_prompt.txt"
280
+ PyAgent
281
+ ```
282
+
283
+ Or edit the default prompt file directly:
284
+
285
+ ```bash
286
+ mkdir -p ~/.pyagent
287
+ $EDITOR ~/.pyagent/system_prompt.txt
288
+ ```
289
+
290
+ A few useful notes:
291
+
292
+ - `/prompt` shows the currently active system prompt inside the TUI.
293
+ - 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.
294
+ - Project and user instruction files (`AGENTS.md`, `skills/**`, `*.skill`) are layered onto the base system prompt automatically.
295
+
296
+ ## Custom tools and skills
297
+
298
+ 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.
299
+
300
+ ### Layout
301
+
302
+ ```text
303
+ ~/.pyagent/
304
+ ├── models.json # named model profiles (existing)
305
+ ├── AGENTS.md # optional user-global agent instructions
306
+ ├── skills/ # user-global skills (*.md, *.skill)
307
+ └── tools/ # user tools (one UV script per tool)
308
+ ├── <my_tool>.py
309
+ ├── disabled/ # listed in /tools but not registered
310
+ └── .cache/manifests.json # auto schema cache (path+mtime+size keyed)
311
+ ```
312
+
313
+ ### Custom tools (UV scripts with click subcommands)
314
+
315
+ Each user tool is a single self-contained Python file. PyAgent runs it through [`uv`](https://docs.astral.sh/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.
316
+
317
+ Every tool must implement two CLI subcommands:
318
+
319
+ - `<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.
320
+ - `<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`.
321
+
322
+ 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:
323
+
324
+ ```python
325
+ #!/usr/bin/env -S uv run --script
326
+ # /// script
327
+ # requires-python = ">=3.10"
328
+ # dependencies = ["click", "huggingface_hub", "datasets"]
329
+ # ///
330
+ import json
331
+ import sys
332
+ from pathlib import Path
333
+ import click
334
+
335
+
336
+ @click.group()
337
+ def cli():
338
+ pass
339
+
340
+
341
+ @cli.command()
342
+ def describe():
343
+ click.echo(json.dumps({
344
+ "name": "my_tool",
345
+ "description": "What this tool does — sent verbatim to the model.",
346
+ "parameters": {
347
+ "type": "object",
348
+ "properties": {"input": {"type": "string"}},
349
+ "required": ["input"],
350
+ },
351
+ "version": "1",
352
+ }))
353
+
354
+
355
+ @cli.command()
356
+ @click.option("--args-file", required=True, type=click.Path(exists=True, path_type=Path))
357
+ def invoke(args_file):
358
+ args = json.loads(args_file.read_text())
359
+ click.echo(my_logic(**args))
360
+
361
+
362
+ if __name__ == "__main__":
363
+ cli()
364
+ ```
365
+
366
+ ### Reference example: `search_hf_datasets`
367
+
368
+ `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:
369
+
370
+ ```bash
371
+ mkdir -p ~/.pyagent/tools
372
+ cp examples/tools/search_hf_datasets.py ~/.pyagent/tools/
373
+ ```
374
+
375
+ 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.
376
+
377
+ ### Lifecycle
378
+
379
+ - 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.
380
+ - Temporarily turn a tool off: `/tools disable <name>` moves it to `~/.pyagent/tools/disabled/` (still listed in `/tools`, not registered).
381
+ - Re-enable: `/tools enable <name>`.
382
+ - Locate a script: `/tools open <name>` prints the absolute path.
383
+ - 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.
384
+ - Bad scripts (timeout, non-zero `describe`, malformed JSON) are listed under "Broken external tools" and skipped; healthy tools keep loading.
385
+ - Missing `uv`: external tools are disabled at startup with a clear banner; built-ins continue to work.
386
+
387
+ ### Custom skills and `AGENTS.md`
388
+
389
+ `~/.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.
390
+
391
+ ### Trust boundary
392
+
393
+ `~/.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.
394
+
395
+ ## Quick CLI smoke test
396
+
397
+ ```bash
398
+ python test_agent.py
399
+ ```
400
+
401
+ ## Development test commands
402
+
403
+ For non-trivial changes, run:
404
+
405
+ ```bash
406
+ python -m py_compile pyagent/*.py test_agent.py
407
+ python -m unittest -v
408
+ ```