agentwatcher 0.1.0__py3-none-any.whl
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.
- agentwatch/__init__.py +14 -0
- agentwatch/sdk.py +130 -0
- agentwatcher-0.1.0.dist-info/METADATA +170 -0
- agentwatcher-0.1.0.dist-info/RECORD +5 -0
- agentwatcher-0.1.0.dist-info/WHEEL +4 -0
agentwatch/__init__.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""agentwatch — DevTools-style overlay for browser-based AI agents.
|
|
2
|
+
|
|
3
|
+
Public API:
|
|
4
|
+
from agentwatch import AgentWatch
|
|
5
|
+
|
|
6
|
+
with AgentWatch() as aw:
|
|
7
|
+
aw.announce(goal="Book a flight from SFO to JFK")
|
|
8
|
+
aw.announce(last_action="Clicked search button", next_action="Type 'SFO'")
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from .sdk import AgentWatch, announce, start, stop
|
|
12
|
+
|
|
13
|
+
__all__ = ["AgentWatch", "announce", "start", "stop"]
|
|
14
|
+
__version__ = "0.1.0"
|
agentwatch/sdk.py
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Public SDK — spawn a WS broadcast server in a background thread and
|
|
2
|
+
let agent code push status events to any connected agentwatch overlay."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
import json
|
|
8
|
+
import threading
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
import websockets
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class _Broadcaster:
|
|
15
|
+
def __init__(self, host: str, port: int) -> None:
|
|
16
|
+
self.host = host
|
|
17
|
+
self.port = port
|
|
18
|
+
self.clients: set = set()
|
|
19
|
+
self.last_state: dict = {}
|
|
20
|
+
self.loop: Optional[asyncio.AbstractEventLoop] = None
|
|
21
|
+
self.server = None
|
|
22
|
+
self._thread: Optional[threading.Thread] = None
|
|
23
|
+
self._ready = threading.Event()
|
|
24
|
+
|
|
25
|
+
async def _handler(self, ws):
|
|
26
|
+
self.clients.add(ws)
|
|
27
|
+
if self.last_state:
|
|
28
|
+
try:
|
|
29
|
+
await ws.send(json.dumps(self.last_state))
|
|
30
|
+
except Exception:
|
|
31
|
+
pass
|
|
32
|
+
try:
|
|
33
|
+
async for _msg in ws:
|
|
34
|
+
pass
|
|
35
|
+
finally:
|
|
36
|
+
self.clients.discard(ws)
|
|
37
|
+
|
|
38
|
+
async def _serve(self) -> None:
|
|
39
|
+
self.server = await websockets.serve(self._handler, self.host, self.port)
|
|
40
|
+
self._ready.set()
|
|
41
|
+
await self.server.wait_closed()
|
|
42
|
+
|
|
43
|
+
def start(self) -> None:
|
|
44
|
+
def run() -> None:
|
|
45
|
+
self.loop = asyncio.new_event_loop()
|
|
46
|
+
asyncio.set_event_loop(self.loop)
|
|
47
|
+
try:
|
|
48
|
+
self.loop.run_until_complete(self._serve())
|
|
49
|
+
except OSError:
|
|
50
|
+
self._ready.set() # release waiter even if port is taken
|
|
51
|
+
self._thread = threading.Thread(target=run, name="agentwatch-ws", daemon=True)
|
|
52
|
+
self._thread.start()
|
|
53
|
+
self._ready.wait(timeout=2.0)
|
|
54
|
+
|
|
55
|
+
def broadcast(self, payload: dict) -> None:
|
|
56
|
+
if not self.loop or not self.loop.is_running():
|
|
57
|
+
return
|
|
58
|
+
merged = {**self.last_state, **payload}
|
|
59
|
+
self.last_state = merged
|
|
60
|
+
message = json.dumps(merged)
|
|
61
|
+
|
|
62
|
+
async def send_all() -> None:
|
|
63
|
+
dead = []
|
|
64
|
+
for client in list(self.clients):
|
|
65
|
+
try:
|
|
66
|
+
await client.send(message)
|
|
67
|
+
except Exception:
|
|
68
|
+
dead.append(client)
|
|
69
|
+
for d in dead:
|
|
70
|
+
self.clients.discard(d)
|
|
71
|
+
asyncio.run_coroutine_threadsafe(send_all(), self.loop)
|
|
72
|
+
|
|
73
|
+
def stop(self) -> None:
|
|
74
|
+
if self.server and self.loop:
|
|
75
|
+
self.loop.call_soon_threadsafe(self.server.close)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
_singleton: Optional[_Broadcaster] = None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def start(host: str = "127.0.0.1", port: int = 8765) -> None:
|
|
82
|
+
"""Start the WebSocket broadcaster (idempotent)."""
|
|
83
|
+
global _singleton
|
|
84
|
+
if _singleton is not None:
|
|
85
|
+
return
|
|
86
|
+
_singleton = _Broadcaster(host=host, port=port)
|
|
87
|
+
_singleton.start()
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def stop() -> None:
|
|
91
|
+
"""Stop the broadcaster, releasing the port."""
|
|
92
|
+
global _singleton
|
|
93
|
+
if _singleton is not None:
|
|
94
|
+
_singleton.stop()
|
|
95
|
+
_singleton = None
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def announce(**fields) -> None:
|
|
99
|
+
"""Push a status update to every connected agentwatch overlay.
|
|
100
|
+
|
|
101
|
+
Recognized keys (any subset): goal, last_action, next_action, step, error.
|
|
102
|
+
Arbitrary keys also work but won't be rendered by the default overlay.
|
|
103
|
+
"""
|
|
104
|
+
if _singleton is None:
|
|
105
|
+
start()
|
|
106
|
+
assert _singleton is not None
|
|
107
|
+
_singleton.broadcast(fields)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class AgentWatch:
|
|
111
|
+
"""Context manager wrapper. Use as::
|
|
112
|
+
|
|
113
|
+
with AgentWatch() as aw:
|
|
114
|
+
aw.announce(goal="...")
|
|
115
|
+
aw.announce(last_action="clicked X", next_action="type 'hello'")
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
def __init__(self, host: str = "127.0.0.1", port: int = 8765) -> None:
|
|
119
|
+
self.host = host
|
|
120
|
+
self.port = port
|
|
121
|
+
|
|
122
|
+
def __enter__(self) -> "AgentWatch":
|
|
123
|
+
start(self.host, self.port)
|
|
124
|
+
return self
|
|
125
|
+
|
|
126
|
+
def __exit__(self, *_exc) -> None:
|
|
127
|
+
stop()
|
|
128
|
+
|
|
129
|
+
def announce(self, **fields) -> None:
|
|
130
|
+
announce(**fields)
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agentwatcher
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: DevTools overlay for browser-based AI agents — see what your agent is doing in real time.
|
|
5
|
+
Project-URL: Homepage, https://github.com/yubinkim444/agentwatch
|
|
6
|
+
Project-URL: Repository, https://github.com/yubinkim444/agentwatch
|
|
7
|
+
Project-URL: Issues, https://github.com/yubinkim444/agentwatch/issues
|
|
8
|
+
Author: yubinkim444
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
Keywords: ai-agents,browser-agent,browser-use,debugging,developer-tools,playwright,selenium
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Software Development :: Debuggers
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Requires-Dist: websockets>=12.0
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# agentwatch
|
|
25
|
+
|
|
26
|
+
> **React DevTools, for browser AI agents.**
|
|
27
|
+
> Drop a translucent overlay onto every tab that shows what your agent is
|
|
28
|
+
> trying to do, what it just clicked, and what it's about to do next.
|
|
29
|
+
|
|
30
|
+
[](https://pypi.org/project/agentwatch/)
|
|
31
|
+
[]()
|
|
32
|
+
[]()
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Why
|
|
37
|
+
|
|
38
|
+
Browser-use, Playwright agents, computer-use models — they're all **black
|
|
39
|
+
boxes** to the developer watching them. The agent moves the mouse, types
|
|
40
|
+
into a field, the page reloads, then it errors out, and you're left
|
|
41
|
+
guessing what it thought it was doing.
|
|
42
|
+
|
|
43
|
+
`agentwatch` adds a tiny floating overlay to every tab. Your agent code
|
|
44
|
+
pushes one-line status updates ("goal: book a flight", "last action:
|
|
45
|
+
clicked search", "next action: type SFO"), and the overlay shows them
|
|
46
|
+
live, framework-agnostic.
|
|
47
|
+
|
|
48
|
+
The result: bug screenshots that are **self-explanatory**. Demo GIFs that
|
|
49
|
+
viewers actually understand. Pair-debugging sessions where the human can
|
|
50
|
+
intervene the moment the agent goes off-rails.
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Architecture
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
┌──────────────────────────┐ ws://127.0.0.1:8765 ┌──────────────────────────┐
|
|
58
|
+
│ Your agent (any lang) │ ──────────────────────────────▶ │ Chrome MV3 extension │
|
|
59
|
+
│ agentwatch.announce( │ broadcasts status JSON │ injects floating overlay│
|
|
60
|
+
│ goal=..., last=...) │ │ on every tab │
|
|
61
|
+
└──────────────────────────┘ └──────────────────────────┘
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The SDK runs a localhost WebSocket broadcaster in a background thread.
|
|
65
|
+
The extension's content script connects to it on every page load. Each
|
|
66
|
+
`announce()` call updates the overlay in place with a green flash.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Install (2 steps)
|
|
71
|
+
|
|
72
|
+
### 1. Install the Chrome extension
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
git clone https://github.com/yubinkim444/agentwatch.git
|
|
76
|
+
cd agentwatch
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
In Chrome / Edge / Brave:
|
|
80
|
+
- Open `chrome://extensions`
|
|
81
|
+
- Toggle **Developer mode** (top-right)
|
|
82
|
+
- Click **Load unpacked**
|
|
83
|
+
- Select the `extension/` folder
|
|
84
|
+
|
|
85
|
+
A small overlay appears in the bottom-right of every tab, showing a
|
|
86
|
+
disconnected (yellow) dot until your agent connects.
|
|
87
|
+
|
|
88
|
+
### 2. Install the SDK
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
pip install agentwatcher
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Use it
|
|
97
|
+
|
|
98
|
+
### Python
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
from agentwatch import AgentWatch
|
|
102
|
+
|
|
103
|
+
with AgentWatch() as aw:
|
|
104
|
+
aw.announce(goal="Book a one-way flight SFO → JFK for tomorrow")
|
|
105
|
+
|
|
106
|
+
# ...do agent stuff...
|
|
107
|
+
|
|
108
|
+
aw.announce(step=1, last_action="Opened google.com/flights",
|
|
109
|
+
next_action="Click the origin field")
|
|
110
|
+
# ...
|
|
111
|
+
aw.announce(step=2, last_action="Typed 'SFO'",
|
|
112
|
+
next_action="Click the destination field")
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Or imperatively, without the context manager:
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
import agentwatch
|
|
119
|
+
agentwatch.start()
|
|
120
|
+
agentwatch.announce(goal="...", last_action="...", next_action="...")
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Any other language
|
|
124
|
+
|
|
125
|
+
Open a WebSocket to `ws://127.0.0.1:8765` and send JSON like:
|
|
126
|
+
|
|
127
|
+
```json
|
|
128
|
+
{"goal": "Book a flight", "last_action": "Clicked search", "next_action": "Type SFO"}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Every connected tab's overlay updates immediately.
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## Recognized fields
|
|
136
|
+
|
|
137
|
+
| Field | What the overlay shows |
|
|
138
|
+
|-------|------------------------|
|
|
139
|
+
| `goal` | Top-line objective. |
|
|
140
|
+
| `last_action` | What the agent just did. |
|
|
141
|
+
| `next_action` | What the agent is about to do. |
|
|
142
|
+
| `step` | Optional step counter / index. |
|
|
143
|
+
| `error` | Highlighted in red. The error row only appears when set. |
|
|
144
|
+
|
|
145
|
+
Any other keys are kept in the broadcast payload but not rendered by the
|
|
146
|
+
default overlay — fork the extension if you want to add custom rows.
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## Pairs nicely with
|
|
151
|
+
|
|
152
|
+
- [`playwright`](https://playwright.dev), [`selenium`](https://selenium.dev),
|
|
153
|
+
[`browser-use`](https://github.com/browser-use/browser-use), any computer-use agent.
|
|
154
|
+
- A screen-recorder. The overlay + your tool together produce demo GIFs
|
|
155
|
+
that explain themselves.
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## Companion projects
|
|
160
|
+
|
|
161
|
+
- **[mcp-rec](https://github.com/yubinkim444/mcp-rec)** — VCR for MCP servers.
|
|
162
|
+
- **[llm-cache-proxy](https://github.com/yubinkim444/llm-cache-proxy)** — disk cache for OpenAI/Anthropic API calls.
|
|
163
|
+
- **[promptlock](https://github.com/yubinkim444/promptlock)** — lockfile for prompts.
|
|
164
|
+
- **[context-diff](https://github.com/yubinkim444/context-diff)** — git diff for Claude Code context windows.
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## License
|
|
169
|
+
|
|
170
|
+
MIT © yubinkim444
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
agentwatch/__init__.py,sha256=W3K2UdkiZ8mZCL50ePG47SQTK-bSvSogHxE2VtXgBpA,425
|
|
2
|
+
agentwatch/sdk.py,sha256=9w445iJfRhHO-IY5VYNEgwOBVZ2f7q0E0u6-jCsdNcI,3842
|
|
3
|
+
agentwatcher-0.1.0.dist-info/METADATA,sha256=YLgjeo-aqMNHqxRnGgPKiDnEbifpyV3jviEIYQMD3-A,5755
|
|
4
|
+
agentwatcher-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
5
|
+
agentwatcher-0.1.0.dist-info/RECORD,,
|