aihook 0.1.5__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.
- aihook-0.1.5/.github/workflows/python-app.yml +79 -0
- aihook-0.1.5/.gitignore +7 -0
- aihook-0.1.5/LICENSE +21 -0
- aihook-0.1.5/MANIFEST.in +2 -0
- aihook-0.1.5/PKG-INFO +198 -0
- aihook-0.1.5/README.md +180 -0
- aihook-0.1.5/improvement-plan.md +199 -0
- aihook-0.1.5/pyproject.toml +42 -0
- aihook-0.1.5/requirements.txt +4 -0
- aihook-0.1.5/setup.cfg +4 -0
- aihook-0.1.5/src/aihook/SKILL.md +180 -0
- aihook-0.1.5/src/aihook/__init__.py +28 -0
- aihook-0.1.5/src/aihook/cli.py +456 -0
- aihook-0.1.5/src/aihook/core.py +481 -0
- aihook-0.1.5/src/aihook/learnings/README.md +7 -0
- aihook-0.1.5/src/aihook/release.py +2 -0
- aihook-0.1.5/src/aihook.egg-info/PKG-INFO +198 -0
- aihook-0.1.5/src/aihook.egg-info/SOURCES.txt +26 -0
- aihook-0.1.5/src/aihook.egg-info/dependency_links.txt +1 -0
- aihook-0.1.5/src/aihook.egg-info/entry_points.txt +2 -0
- aihook-0.1.5/src/aihook.egg-info/requires.txt +4 -0
- aihook-0.1.5/src/aihook.egg-info/top_level.txt +1 -0
- aihook-0.1.5/tests/__init__.py +0 -0
- aihook-0.1.5/tests/conftest.py +25 -0
- aihook-0.1.5/tests/test_core.py +116 -0
- aihook-0.1.5/tests/test_integration.py +433 -0
- aihook-0.1.5/tests/the-test-script.py +22 -0
- aihook-0.1.5/tool-description.md +109 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# This workflow will install Python dependencies, run tests and lint with a single version of Python
|
|
2
|
+
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
|
|
3
|
+
|
|
4
|
+
name: CI
|
|
5
|
+
|
|
6
|
+
on:
|
|
7
|
+
push:
|
|
8
|
+
branches:
|
|
9
|
+
- main
|
|
10
|
+
- develop
|
|
11
|
+
pull_request:
|
|
12
|
+
branches:
|
|
13
|
+
- main
|
|
14
|
+
- develop
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
build:
|
|
18
|
+
|
|
19
|
+
runs-on: ubuntu-latest
|
|
20
|
+
|
|
21
|
+
env:
|
|
22
|
+
# note: 'true' only works if no error occurred.
|
|
23
|
+
# DEBUG_WITH_SSH: 'true'
|
|
24
|
+
DEBUG_WITH_SSH: false
|
|
25
|
+
|
|
26
|
+
steps:
|
|
27
|
+
- uses: actions/checkout@v4
|
|
28
|
+
|
|
29
|
+
- name: Set up Python 3.11
|
|
30
|
+
uses: actions/setup-python@v4
|
|
31
|
+
with:
|
|
32
|
+
python-version: "3.11"
|
|
33
|
+
|
|
34
|
+
# we use uv because it is much faster than normal pip
|
|
35
|
+
- name: Install uv
|
|
36
|
+
run: |
|
|
37
|
+
python -m pip install uv
|
|
38
|
+
|
|
39
|
+
- name: Installing requirements and package
|
|
40
|
+
run: |
|
|
41
|
+
# install this package
|
|
42
|
+
pwd
|
|
43
|
+
uv pip install --system -r requirements.txt
|
|
44
|
+
uv pip install --system -e .
|
|
45
|
+
|
|
46
|
+
- name: configure git
|
|
47
|
+
run: |
|
|
48
|
+
git config --global user.email "ci-user@ci-server.com"
|
|
49
|
+
git config --global user.name "CI User"
|
|
50
|
+
git config --global init.defaultBranch main
|
|
51
|
+
|
|
52
|
+
- name: run all tests
|
|
53
|
+
run: |
|
|
54
|
+
pytest
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# debugging (set env.DEBUG_WITH_SSH to true (see above))
|
|
58
|
+
- name: Setup tmate session
|
|
59
|
+
if: env.DEBUG_WITH_SSH == 'true'
|
|
60
|
+
id: tmate
|
|
61
|
+
uses: mxschmitt/action-tmate@v3
|
|
62
|
+
with:
|
|
63
|
+
detached: true
|
|
64
|
+
|
|
65
|
+
# this exports the ad-hoc ssh-connection address to an text file such that it can be accessed via API
|
|
66
|
+
# this allows to connect to the debugging ssh server without manual copy-pasting
|
|
67
|
+
# see https://github.com/cknoll/github-ci-ssh-connect
|
|
68
|
+
|
|
69
|
+
- name: Capture tmate output
|
|
70
|
+
if: env.DEBUG_WITH_SSH == 'true'
|
|
71
|
+
run: |
|
|
72
|
+
echo $(cat /home/runner/work/_temp/_runner_file_commands/* | grep ::notice::SSH | awk '{print $NF}') > tmate-connection.txt
|
|
73
|
+
|
|
74
|
+
- name: Upload tmate logs as artifact
|
|
75
|
+
if: env.DEBUG_WITH_SSH == 'true'
|
|
76
|
+
uses: actions/upload-artifact@v4
|
|
77
|
+
with:
|
|
78
|
+
name: tmate-connection
|
|
79
|
+
path: tmate-connection.txt
|
aihook-0.1.5/.gitignore
ADDED
aihook-0.1.5/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 C. Knoll
|
|
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.
|
aihook-0.1.5/MANIFEST.in
ADDED
aihook-0.1.5/PKG-INFO
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aihook
|
|
3
|
+
Version: 0.1.5
|
|
4
|
+
Summary: interactive embedded shell for agents
|
|
5
|
+
Author-email: Carsten Knoll <firstname.lastname@domain.org>
|
|
6
|
+
License-Expression: GPL-3.0-or-later
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Requires-Python: >=3.11
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Requires-Dist: ipydex
|
|
14
|
+
Requires-Dist: pytest
|
|
15
|
+
Requires-Dist: PyYAML
|
|
16
|
+
Requires-Dist: platformdirs
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
# aihook
|
|
20
|
+
|
|
21
|
+
**Pause a running Python script and let an AI agent inspect and manipulate its live state — without restarting.**
|
|
22
|
+
|
|
23
|
+
[](https://github.com/psf/black)
|
|
24
|
+
[](https://www.python.org/)
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## The problem
|
|
29
|
+
|
|
30
|
+
Some scripts are expensive to restart. A Selenium browser session takes 60 seconds to reach the
|
|
31
|
+
interesting state. An ML pipeline loads a 4 GB model before the first inference. A scraper
|
|
32
|
+
re-authenticates and paginates through 200 pages before getting to the data you care about.
|
|
33
|
+
|
|
34
|
+
When debugging these scripts, the standard loop — edit, restart, wait, observe — is brutal.
|
|
35
|
+
Each hypothesis costs a full restart.
|
|
36
|
+
|
|
37
|
+
## What aihook does
|
|
38
|
+
|
|
39
|
+
Drop one call into your script:
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from aihook import agent_hook
|
|
43
|
+
agent_hook() # script pauses here; HTTP REPL starts
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The script pauses at that line. An HTTP server starts on a free port. The calling frame's
|
|
47
|
+
variables are all live and accessible. An AI agent (or you) can then probe, manipulate, and
|
|
48
|
+
test fixes against the real running state:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
aihook 'browser.find_by_css(".comment__replies")'
|
|
52
|
+
aihook -f snippet.py # run a multi-line snippet, edit, repeat
|
|
53
|
+
aihook --exit # resume the script
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Each probe takes 2–5 seconds instead of 60.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Real-world example
|
|
61
|
+
|
|
62
|
+
This is what a session looks like in practice. The script is a Selenium browser automation that
|
|
63
|
+
was failing silently on every "show replies" button click:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
# Start the slow script in the background
|
|
67
|
+
python fetch_comments.py --url "https://example.com/article" > aihook-host.log 2>&1 &
|
|
68
|
+
|
|
69
|
+
# Wait for it to reach agent_hook() — CLI validates pid + port, times out after 180s
|
|
70
|
+
# aihook: session found after 94.3s
|
|
71
|
+
|
|
72
|
+
# Probe the live DOM
|
|
73
|
+
aihook 'browser.find_by_css("[data-comment-id]")'
|
|
74
|
+
# -> [] ← the attribute does not exist
|
|
75
|
+
|
|
76
|
+
aihook 'browser.find_by_css("[id^=cid-]")'
|
|
77
|
+
# -> [<Element id="cid-4821"> ... ] ← found it
|
|
78
|
+
|
|
79
|
+
# Try the JS click that works despite the overlay
|
|
80
|
+
aihook -f snippet.py # snippet tests execute_script("arguments[0].click()", btn)
|
|
81
|
+
# -> reply count went from 8 to 28
|
|
82
|
+
|
|
83
|
+
aihook --exit
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Five probes. Zero restarts. A working script at the end.
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Install
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
pip install aihook
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Set up the AI agent skill
|
|
97
|
+
|
|
98
|
+
For Claude Code:
|
|
99
|
+
```bash
|
|
100
|
+
aihook --bootstrap --agent claude
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
For aider / aider-desk:
|
|
104
|
+
```bash
|
|
105
|
+
aihook --bootstrap --agent aider
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
This installs `SKILL.md` (the agent's instruction file for using aihook) and creates a
|
|
109
|
+
`learnings/` directory where agent-accumulated tips are stored across sessions.
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## CLI reference
|
|
114
|
+
|
|
115
|
+
| Command | Description |
|
|
116
|
+
|---|---|
|
|
117
|
+
| `aihook '<code>'` | Execute code; single expressions auto-print their `repr()` |
|
|
118
|
+
| `aihook -f FILE` | Send contents of FILE (`-f -` reads from stdin) |
|
|
119
|
+
| `aihook --exit` | Shut down the session and let the script resume |
|
|
120
|
+
| `aihook --status` | Show whether a session is active (exits 0 if healthy) |
|
|
121
|
+
| `aihook --clean` | Remove a stale lock file |
|
|
122
|
+
| `aihook --wait N` | Wait up to N seconds for a healthy session (default: 180s) |
|
|
123
|
+
| `aihook -p PORT` | Target a specific port (skips lock-file discovery) |
|
|
124
|
+
| `aihook --lockfile PATH` | Use a custom lock-file path |
|
|
125
|
+
| `aihook --bootstrap` | Install SKILL.md and create learnings directory |
|
|
126
|
+
|
|
127
|
+
Exit code is non-zero if the remote code raised an exception or wrote to stderr.
|
|
128
|
+
|
|
129
|
+
**Auto-print:** if the submitted code is a single expression, its `repr()` is printed
|
|
130
|
+
automatically. For multi-line snippets, use explicit `print()` — auto-print only applies to
|
|
131
|
+
single expressions.
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## How it works
|
|
136
|
+
|
|
137
|
+
`agent_hook()` does three things:
|
|
138
|
+
|
|
139
|
+
1. Captures the calling frame's `globals` and `locals` into a shared namespace.
|
|
140
|
+
2. Starts a minimal HTTP server on a free port in `5001–5101` (configurable).
|
|
141
|
+
3. Writes `./aihook-lock.yml` containing `pid`, `port`, `cwd`, and `start_time`.
|
|
142
|
+
|
|
143
|
+
The CLI reads the lock file to find the port, POSTs code to `/execute`, and prints the
|
|
144
|
+
result. The server shuts down on `exit()`, the lock file is removed, and the host script
|
|
145
|
+
resumes.
|
|
146
|
+
|
|
147
|
+
The banner printed on startup includes the source file and line number where `agent_hook()`
|
|
148
|
+
was called — useful when a script has multiple hook points.
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## One caveat: local variable write-back
|
|
153
|
+
|
|
154
|
+
Rebinding a **local** variable of the calling function from inside the REPL does **not**
|
|
155
|
+
write back to that function's fast-locals. This is the same limitation as `pdb`:
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
# Inside my_function():
|
|
159
|
+
x = 1
|
|
160
|
+
agent_hook()
|
|
161
|
+
print(x) # still prints 1 even if you did `x = 2` via aihook
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
**Mutating mutable objects works fine:**
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
my_list.append(99) # visible in the host afterwards
|
|
168
|
+
my_dict["key"] = "new" # same
|
|
169
|
+
obj.attribute = "changed" # same
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
When testing a fix, mutate containers or attributes rather than rebinding local names.
|
|
173
|
+
|
|
174
|
+
(Of course, this is documented for agents in [SKILL.md](src/aihook/SKILL.md).)
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## Optional environment variables
|
|
179
|
+
|
|
180
|
+
| Variable | Effect |
|
|
181
|
+
|---|---|
|
|
182
|
+
| `AIHOOK_PORT=NNNN` | Force a specific port |
|
|
183
|
+
| `AIHOOK_PORT_RANGE=LO-HI` | Override the default `5001-5101` range |
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## Development
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
git clone <repo-url>
|
|
191
|
+
cd aihook
|
|
192
|
+
pip install -e .
|
|
193
|
+
pytest
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
The learnings directory (created by `--bootstrap`) accumulates session tips in
|
|
197
|
+
`~/.local/share/aihook/learnings/` (Linux) or the platform equivalent. Add
|
|
198
|
+
`topic_<name>.md` files there to share domain-specific knowledge across agent sessions.
|
aihook-0.1.5/README.md
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# aihook
|
|
2
|
+
|
|
3
|
+
**Pause a running Python script and let an AI agent inspect and manipulate its live state — without restarting.**
|
|
4
|
+
|
|
5
|
+
[](https://github.com/psf/black)
|
|
6
|
+
[](https://www.python.org/)
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## The problem
|
|
11
|
+
|
|
12
|
+
Some scripts are expensive to restart. A Selenium browser session takes 60 seconds to reach the
|
|
13
|
+
interesting state. An ML pipeline loads a 4 GB model before the first inference. A scraper
|
|
14
|
+
re-authenticates and paginates through 200 pages before getting to the data you care about.
|
|
15
|
+
|
|
16
|
+
When debugging these scripts, the standard loop — edit, restart, wait, observe — is brutal.
|
|
17
|
+
Each hypothesis costs a full restart.
|
|
18
|
+
|
|
19
|
+
## What aihook does
|
|
20
|
+
|
|
21
|
+
Drop one call into your script:
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
from aihook import agent_hook
|
|
25
|
+
agent_hook() # script pauses here; HTTP REPL starts
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The script pauses at that line. An HTTP server starts on a free port. The calling frame's
|
|
29
|
+
variables are all live and accessible. An AI agent (or you) can then probe, manipulate, and
|
|
30
|
+
test fixes against the real running state:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
aihook 'browser.find_by_css(".comment__replies")'
|
|
34
|
+
aihook -f snippet.py # run a multi-line snippet, edit, repeat
|
|
35
|
+
aihook --exit # resume the script
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Each probe takes 2–5 seconds instead of 60.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Real-world example
|
|
43
|
+
|
|
44
|
+
This is what a session looks like in practice. The script is a Selenium browser automation that
|
|
45
|
+
was failing silently on every "show replies" button click:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
# Start the slow script in the background
|
|
49
|
+
python fetch_comments.py --url "https://example.com/article" > aihook-host.log 2>&1 &
|
|
50
|
+
|
|
51
|
+
# Wait for it to reach agent_hook() — CLI validates pid + port, times out after 180s
|
|
52
|
+
# aihook: session found after 94.3s
|
|
53
|
+
|
|
54
|
+
# Probe the live DOM
|
|
55
|
+
aihook 'browser.find_by_css("[data-comment-id]")'
|
|
56
|
+
# -> [] ← the attribute does not exist
|
|
57
|
+
|
|
58
|
+
aihook 'browser.find_by_css("[id^=cid-]")'
|
|
59
|
+
# -> [<Element id="cid-4821"> ... ] ← found it
|
|
60
|
+
|
|
61
|
+
# Try the JS click that works despite the overlay
|
|
62
|
+
aihook -f snippet.py # snippet tests execute_script("arguments[0].click()", btn)
|
|
63
|
+
# -> reply count went from 8 to 28
|
|
64
|
+
|
|
65
|
+
aihook --exit
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Five probes. Zero restarts. A working script at the end.
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## Install
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
pip install aihook
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Set up the AI agent skill
|
|
79
|
+
|
|
80
|
+
For Claude Code:
|
|
81
|
+
```bash
|
|
82
|
+
aihook --bootstrap --agent claude
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
For aider / aider-desk:
|
|
86
|
+
```bash
|
|
87
|
+
aihook --bootstrap --agent aider
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
This installs `SKILL.md` (the agent's instruction file for using aihook) and creates a
|
|
91
|
+
`learnings/` directory where agent-accumulated tips are stored across sessions.
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## CLI reference
|
|
96
|
+
|
|
97
|
+
| Command | Description |
|
|
98
|
+
|---|---|
|
|
99
|
+
| `aihook '<code>'` | Execute code; single expressions auto-print their `repr()` |
|
|
100
|
+
| `aihook -f FILE` | Send contents of FILE (`-f -` reads from stdin) |
|
|
101
|
+
| `aihook --exit` | Shut down the session and let the script resume |
|
|
102
|
+
| `aihook --status` | Show whether a session is active (exits 0 if healthy) |
|
|
103
|
+
| `aihook --clean` | Remove a stale lock file |
|
|
104
|
+
| `aihook --wait N` | Wait up to N seconds for a healthy session (default: 180s) |
|
|
105
|
+
| `aihook -p PORT` | Target a specific port (skips lock-file discovery) |
|
|
106
|
+
| `aihook --lockfile PATH` | Use a custom lock-file path |
|
|
107
|
+
| `aihook --bootstrap` | Install SKILL.md and create learnings directory |
|
|
108
|
+
|
|
109
|
+
Exit code is non-zero if the remote code raised an exception or wrote to stderr.
|
|
110
|
+
|
|
111
|
+
**Auto-print:** if the submitted code is a single expression, its `repr()` is printed
|
|
112
|
+
automatically. For multi-line snippets, use explicit `print()` — auto-print only applies to
|
|
113
|
+
single expressions.
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## How it works
|
|
118
|
+
|
|
119
|
+
`agent_hook()` does three things:
|
|
120
|
+
|
|
121
|
+
1. Captures the calling frame's `globals` and `locals` into a shared namespace.
|
|
122
|
+
2. Starts a minimal HTTP server on a free port in `5001–5101` (configurable).
|
|
123
|
+
3. Writes `./aihook-lock.yml` containing `pid`, `port`, `cwd`, and `start_time`.
|
|
124
|
+
|
|
125
|
+
The CLI reads the lock file to find the port, POSTs code to `/execute`, and prints the
|
|
126
|
+
result. The server shuts down on `exit()`, the lock file is removed, and the host script
|
|
127
|
+
resumes.
|
|
128
|
+
|
|
129
|
+
The banner printed on startup includes the source file and line number where `agent_hook()`
|
|
130
|
+
was called — useful when a script has multiple hook points.
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## One caveat: local variable write-back
|
|
135
|
+
|
|
136
|
+
Rebinding a **local** variable of the calling function from inside the REPL does **not**
|
|
137
|
+
write back to that function's fast-locals. This is the same limitation as `pdb`:
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
# Inside my_function():
|
|
141
|
+
x = 1
|
|
142
|
+
agent_hook()
|
|
143
|
+
print(x) # still prints 1 even if you did `x = 2` via aihook
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
**Mutating mutable objects works fine:**
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
my_list.append(99) # visible in the host afterwards
|
|
150
|
+
my_dict["key"] = "new" # same
|
|
151
|
+
obj.attribute = "changed" # same
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
When testing a fix, mutate containers or attributes rather than rebinding local names.
|
|
155
|
+
|
|
156
|
+
(Of course, this is documented for agents in [SKILL.md](src/aihook/SKILL.md).)
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## Optional environment variables
|
|
161
|
+
|
|
162
|
+
| Variable | Effect |
|
|
163
|
+
|---|---|
|
|
164
|
+
| `AIHOOK_PORT=NNNN` | Force a specific port |
|
|
165
|
+
| `AIHOOK_PORT_RANGE=LO-HI` | Override the default `5001-5101` range |
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## Development
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
git clone <repo-url>
|
|
173
|
+
cd aihook
|
|
174
|
+
pip install -e .
|
|
175
|
+
pytest
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
The learnings directory (created by `--bootstrap`) accumulates session tips in
|
|
179
|
+
`~/.local/share/aihook/learnings/` (Linux) or the platform equivalent. Add
|
|
180
|
+
`topic_<name>.md` files there to share domain-specific knowledge across agent sessions.
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# aihook — Improvement Plan
|
|
2
|
+
|
|
3
|
+
Derived from a real first-time agent session and a follow-up Q&A with the testing agent.
|
|
4
|
+
|
|
5
|
+
Items are ordered by value / cost ratio. Each item lists the affected
|
|
6
|
+
file(s), the concrete change, and an acceptance test.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## 1. Fix multi-statement execution (HIGH priority, highest value)
|
|
11
|
+
|
|
12
|
+
### Problem
|
|
13
|
+
|
|
14
|
+
Snippets with more than one top-level statement fail with a confusing
|
|
15
|
+
`SyntaxError`. Reproducer (confirmed by the testing agent):
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
# /tmp/aihook_probe.py
|
|
19
|
+
def foo():
|
|
20
|
+
return 42
|
|
21
|
+
|
|
22
|
+
print(foo())
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
$ aihook -f /tmp/aihook_probe.py
|
|
27
|
+
File "<console>", line 4
|
|
28
|
+
print(foo())
|
|
29
|
+
^^^^^
|
|
30
|
+
SyntaxError: invalid syntax
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Root cause
|
|
34
|
+
|
|
35
|
+
`AgenticREPL.execute_command` in `src/aihook/core.py` first tries
|
|
36
|
+
`compile(command, "<agent>", "eval")`. On `SyntaxError` it falls back to
|
|
37
|
+
`self.console.push(command)`. `InteractiveConsole.push` is **line-oriented**:
|
|
38
|
+
it mimics a human typing at a `>>>` prompt, splitting on newlines and
|
|
39
|
+
feeding lines one at a time through `compile_command`. It cannot execute a
|
|
40
|
+
whole multi-line source string in one call the way `exec(compile(..., "exec"))`
|
|
41
|
+
can — hence the spurious error on the second top-level statement.
|
|
42
|
+
|
|
43
|
+
### Fix
|
|
44
|
+
|
|
45
|
+
In `src/aihook/core.py`, replace the `else` branch of `execute_command`
|
|
46
|
+
(the `self.console.push(command)` call) with a direct `exec` of the
|
|
47
|
+
compiled module:
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
else:
|
|
51
|
+
code_obj = compile(command, "<agent>", "exec")
|
|
52
|
+
exec(code_obj, self.namespace)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Keep the expression-first path unchanged (single expressions still
|
|
56
|
+
auto-print their `repr`). Drop the now-unused `self.console` attribute
|
|
57
|
+
(and the `import code` + `self.console = code.InteractiveConsole(...)` in
|
|
58
|
+
`__init__`) unless we find another use for it — leaner is better.
|
|
59
|
+
|
|
60
|
+
Rationale: the `InteractiveConsole` machinery was only needed for the
|
|
61
|
+
interactive line-by-line prompt semantics, which we do not want. Agents
|
|
62
|
+
send complete, already-edited source fragments; `exec` is the right tool.
|
|
63
|
+
|
|
64
|
+
### Acceptance tests
|
|
65
|
+
|
|
66
|
+
Add to `tests/test_integration.py`:
|
|
67
|
+
|
|
68
|
+
1. Multi-statement block executes and produces the expected stdout:
|
|
69
|
+
```python
|
|
70
|
+
cmd = "def foo():\n return 42\n\nprint(foo())\n"
|
|
71
|
+
# expect stdout == "42\n", stderr == "", exit 0
|
|
72
|
+
```
|
|
73
|
+
2. `def` alone (no trailing call) defines the function in the namespace
|
|
74
|
+
(verify by a follow-up `aihook 'foo()'`).
|
|
75
|
+
3. Assignment + use in one snippet:
|
|
76
|
+
```python
|
|
77
|
+
cmd = "x = 7\nprint(x * 6)\n"
|
|
78
|
+
# expect stdout == "42\n"
|
|
79
|
+
```
|
|
80
|
+
4. Genuine `SyntaxError` in a multi-statement block still reports cleanly
|
|
81
|
+
(non-zero exit, traceback on stderr).
|
|
82
|
+
5. Single-expression path unchanged: `aihook '1+2'` still auto-prints `3`.
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## 2. Print a startup hint about the fast-locals caveat (LOW priority, low cost)
|
|
87
|
+
|
|
88
|
+
### Problem
|
|
89
|
+
|
|
90
|
+
The CPython caveat (rebinding locals of the calling function does not
|
|
91
|
+
propagate back) is documented but remains a trap. Not discoverable at
|
|
92
|
+
runtime.
|
|
93
|
+
|
|
94
|
+
### Fix
|
|
95
|
+
|
|
96
|
+
In `AgenticREPL.run`, after the existing banner lines (the
|
|
97
|
+
`AIHOOK AgenticREPL:` / `AIHOOK_PORT=` block) in `src/aihook/core.py`,
|
|
98
|
+
print one concise stderr hint:
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
sys.stderr.write(
|
|
102
|
+
"AIHOOK: note: rebinding a local variable of the calling function "
|
|
103
|
+
"does not propagate back (CPython fast-locals; same as pdb). "
|
|
104
|
+
"Mutate containers / attributes instead. See SKILL.md.\n"
|
|
105
|
+
)
|
|
106
|
+
sys.stderr.flush()
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Keep the banner itself on stdout unchanged (agents grep for `AIHOOK_PORT=`
|
|
110
|
+
there). The hint goes to stderr so it doesn't pollute machine-parseable
|
|
111
|
+
output.
|
|
112
|
+
|
|
113
|
+
### Acceptance test
|
|
114
|
+
|
|
115
|
+
Integration test asserts the hint substring appears in the host process's
|
|
116
|
+
combined log.
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## 3. Documentation updates in `src/aihook/SKILL.md` (LOW priority, zero risk)
|
|
121
|
+
|
|
122
|
+
Three small additions:
|
|
123
|
+
|
|
124
|
+
### 3a. Promote the iterative `-f FILE` loop as the canonical workflow
|
|
125
|
+
|
|
126
|
+
In the "Canonical agent workflow" section, add an explicit bullet framing
|
|
127
|
+
the dominant debugging pattern:
|
|
128
|
+
|
|
129
|
+
> **Iterative probe loop.** For anything beyond a one-liner, keep a
|
|
130
|
+
> `snippet.py` next to the host script and rerun it after each edit:
|
|
131
|
+
> ```
|
|
132
|
+
> # edit snippet.py
|
|
133
|
+
> aihook -f snippet.py
|
|
134
|
+
> # observe, edit snippet.py again, repeat
|
|
135
|
+
> ```
|
|
136
|
+
> This is the primary workflow; the single-expression form is for quick
|
|
137
|
+
> probes only.
|
|
138
|
+
|
|
139
|
+
### 3b. Document `-f FILE` path resolution
|
|
140
|
+
|
|
141
|
+
Add a note under the CLI reference:
|
|
142
|
+
|
|
143
|
+
> `-f FILE` is opened by the CLI process (agent side), so relative paths
|
|
144
|
+
> resolve against the **agent's** current working directory, not the host
|
|
145
|
+
> script's. Use an absolute path (e.g. `-f "$PWD/snippet.py"`) if you are
|
|
146
|
+
> not certain the cwds match, or if your shell tool chains `cd` commands
|
|
147
|
+
> unreliably.
|
|
148
|
+
|
|
149
|
+
### 3c. Document lock-file discovery scope and manual cleanup
|
|
150
|
+
|
|
151
|
+
Extend the "Session discovery" section with:
|
|
152
|
+
|
|
153
|
+
> Lock-file discovery does **not** walk up parent directories — `aihook`
|
|
154
|
+
> only looks at `./aihook-lock.yml` in the invoking shell's exact cwd.
|
|
155
|
+
> Run `aihook` from the same directory as the host script.
|
|
156
|
+
>
|
|
157
|
+
> If the host process is killed uncleanly (e.g. `SIGKILL`, OOM), the lock
|
|
158
|
+
> file may remain. A subsequent `agent_hook()` call in the same cwd will
|
|
159
|
+
> detect this automatically (pid not alive) and overwrite it. To clean up
|
|
160
|
+
> manually, just `rm ./aihook-lock.yml`.
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## 4. Nice-to-haves (deferred, consider after items 1-3 land)
|
|
165
|
+
|
|
166
|
+
These are not worth doing now but are recorded for future consideration:
|
|
167
|
+
|
|
168
|
+
- **`--echo` / `--verbose` flag** that always prints the last value (or
|
|
169
|
+
`None`) regardless of statement vs. expression. Low value once item 1
|
|
170
|
+
lands, because the agent can just wrap in `print(...)` in a multi-line
|
|
171
|
+
block.
|
|
172
|
+
- **Resolve `-f FILE` relative to `lockfile["cwd"]`** when a relative path
|
|
173
|
+
is given and the file is not found locally. Rejected for now: the CLI
|
|
174
|
+
may run on a different machine than the host, and the current behavior
|
|
175
|
+
is consistent with every other Unix tool that reads files.
|
|
176
|
+
- **Walk up parents for lock-file discovery.** Rejected for now: breaks
|
|
177
|
+
the "one aihook per cwd" invariant and invites cross-project confusion.
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## Out of scope / non-issues confirmed by Q&A
|
|
182
|
+
|
|
183
|
+
- Banner reaches agents via log file reliably (Q4a).
|
|
184
|
+
- `--wait 5` default is a good middle ground (Q4b).
|
|
185
|
+
- Exit-code semantics are correct (Q4c). The `REPL: Exiting` message is
|
|
186
|
+
written to **stdout**, not stderr, so there is no spurious stderr on
|
|
187
|
+
successful `--exit` — the testing agent's suspicion was unfounded.
|
|
188
|
+
- `aihook --exit` shutdown + lock-file cleanup work reliably (Q4d).
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## Implementation order
|
|
193
|
+
|
|
194
|
+
1. Item 1 (exec fix + tests) — single commit.
|
|
195
|
+
2. Item 3 (docs) — single commit, can precede or follow item 1.
|
|
196
|
+
3. Item 2 (startup hint) — single commit.
|
|
197
|
+
|
|
198
|
+
Each item is independently mergeable. Item 1 is the only one with
|
|
199
|
+
behavioural risk; its test suite additions are mandatory.
|