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.
- pyagent_harness-0.1.0/LICENSE +21 -0
- pyagent_harness-0.1.0/PKG-INFO +408 -0
- pyagent_harness-0.1.0/README.md +396 -0
- pyagent_harness-0.1.0/pyagent/__init__.py +1 -0
- pyagent_harness-0.1.0/pyagent/__main__.py +4 -0
- pyagent_harness-0.1.0/pyagent/agent.py +484 -0
- pyagent_harness-0.1.0/pyagent/config.py +162 -0
- pyagent_harness-0.1.0/pyagent/external_tools.py +521 -0
- pyagent_harness-0.1.0/pyagent/llm_client.py +408 -0
- pyagent_harness-0.1.0/pyagent/main.py +25 -0
- pyagent_harness-0.1.0/pyagent/model_profiles.py +210 -0
- pyagent_harness-0.1.0/pyagent/ollama_client.py +3 -0
- pyagent_harness-0.1.0/pyagent/project_context.py +235 -0
- pyagent_harness-0.1.0/pyagent/scaffold.py +65 -0
- pyagent_harness-0.1.0/pyagent/templates/__init__.py +5 -0
- pyagent_harness-0.1.0/pyagent/templates/tool_template.py +121 -0
- pyagent_harness-0.1.0/pyagent/tools.py +610 -0
- pyagent_harness-0.1.0/pyagent/ui.py +1320 -0
- pyagent_harness-0.1.0/pyagent/user_runtime.py +98 -0
- pyagent_harness-0.1.0/pyagent_harness.egg-info/PKG-INFO +408 -0
- pyagent_harness-0.1.0/pyagent_harness.egg-info/SOURCES.txt +25 -0
- pyagent_harness-0.1.0/pyagent_harness.egg-info/dependency_links.txt +1 -0
- pyagent_harness-0.1.0/pyagent_harness.egg-info/entry_points.txt +2 -0
- pyagent_harness-0.1.0/pyagent_harness.egg-info/requires.txt +3 -0
- pyagent_harness-0.1.0/pyagent_harness.egg-info/top_level.txt +1 -0
- pyagent_harness-0.1.0/pyproject.toml +21 -0
- pyagent_harness-0.1.0/setup.cfg +4 -0
|
@@ -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
|
+
```
|