windhover 0.14.2__tar.gz → 0.14.3__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.
- {windhover-0.14.2 → windhover-0.14.3}/PKG-INFO +10 -3
- {windhover-0.14.2 → windhover-0.14.3}/README.md +9 -2
- {windhover-0.14.2 → windhover-0.14.3}/SPEC.md +21 -7
- {windhover-0.14.2 → windhover-0.14.3}/docs/GUIDE.md +17 -0
- {windhover-0.14.2 → windhover-0.14.3}/pyproject.toml +1 -1
- {windhover-0.14.2 → windhover-0.14.3}/windhover/static/index.html +14 -2
- {windhover-0.14.2 → windhover-0.14.3}/.github/workflows/ci.yml +0 -0
- {windhover-0.14.2 → windhover-0.14.3}/.gitignore +0 -0
- {windhover-0.14.2 → windhover-0.14.3}/GUIDE.md +0 -0
- {windhover-0.14.2 → windhover-0.14.3}/LICENSE +0 -0
- {windhover-0.14.2 → windhover-0.14.3}/docs/graph.png +0 -0
- {windhover-0.14.2 → windhover-0.14.3}/docs/logo.svg +0 -0
- {windhover-0.14.2 → windhover-0.14.3}/docs/runs.png +0 -0
- {windhover-0.14.2 → windhover-0.14.3}/docs/social-preview.png +0 -0
- {windhover-0.14.2 → windhover-0.14.3}/docs/stats.png +0 -0
- {windhover-0.14.2 → windhover-0.14.3}/docs/trace.png +0 -0
- {windhover-0.14.2 → windhover-0.14.3}/tests/__init__.py +0 -0
- {windhover-0.14.2 → windhover-0.14.3}/tests/test_smoke.py +0 -0
- {windhover-0.14.2 → windhover-0.14.3}/windhover/__init__.py +0 -0
- {windhover-0.14.2 → windhover-0.14.3}/windhover/config.py +0 -0
- {windhover-0.14.2 → windhover-0.14.3}/windhover/demo_graph.py +0 -0
- {windhover-0.14.2 → windhover-0.14.3}/windhover/demo_rag.py +0 -0
- {windhover-0.14.2 → windhover-0.14.3}/windhover/extract.py +0 -0
- {windhover-0.14.2 → windhover-0.14.3}/windhover/pricing.json +0 -0
- {windhover-0.14.2 → windhover-0.14.3}/windhover/server.py +0 -0
- {windhover-0.14.2 → windhover-0.14.3}/windhover/static/icon.svg +0 -0
- {windhover-0.14.2 → windhover-0.14.3}/windhover/static/manifest.json +0 -0
- {windhover-0.14.2 → windhover-0.14.3}/windhover/static/vendor/cytoscape-dagre.js +0 -0
- {windhover-0.14.2 → windhover-0.14.3}/windhover/static/vendor/cytoscape.min.js +0 -0
- {windhover-0.14.2 → windhover-0.14.3}/windhover/static/vendor/dagre.min.js +0 -0
- {windhover-0.14.2 → windhover-0.14.3}/windhover/static/vendor/fonts/fraunces-500.woff2 +0 -0
- {windhover-0.14.2 → windhover-0.14.3}/windhover/static/vendor/fonts/fraunces-600-italic.woff2 +0 -0
- {windhover-0.14.2 → windhover-0.14.3}/windhover/static/vendor/fonts/fraunces-600.woff2 +0 -0
- {windhover-0.14.2 → windhover-0.14.3}/windhover/store.py +0 -0
- {windhover-0.14.2 → windhover-0.14.3}/windhover/tracer.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: windhover
|
|
3
|
-
Version: 0.14.
|
|
3
|
+
Version: 0.14.3
|
|
4
4
|
Summary: Self-hosted, mobile-friendly observability for LangGraph — traces, run history, cost, and a living graph view.
|
|
5
5
|
Project-URL: Repository, https://github.com/justfeltlikerunning/windhover
|
|
6
6
|
Author: justfeltlikerunning
|
|
@@ -117,6 +117,11 @@ graph.invoke(input, config={
|
|
|
117
117
|
- **Run history + replay** — SQLite; runs persist even if the browser closes (worker thread).
|
|
118
118
|
- **Living graph** — file watcher re-extracts topology in a subprocess and pushes it to the UI.
|
|
119
119
|
- **Dashboards** — runs/tokens per day, per-model usage and latency, per-node latency, error rate.
|
|
120
|
+
- **Multi-graph** — serve every graph in your project behind one URL; a top-bar selector
|
|
121
|
+
scopes all views (runs, sessions, stats included — metrics never mix graphs).
|
|
122
|
+
- **Alerts** — `WINDHOVER_WEBHOOK` pushes a JSON summary when a run errors or pauses.
|
|
123
|
+
- **Never slows your app** — the remote tracer is non-blocking (bounded queue; drops
|
|
124
|
+
rather than delays when the collector is down).
|
|
120
125
|
- **Mobile-first PWA**, light/dark. Fully local (FastAPI + Cytoscape.js).
|
|
121
126
|
|
|
122
127
|
## Datasets API
|
|
@@ -135,11 +140,13 @@ curl -X POST :8090/api/runs/RUN_ID/scores -H 'Content-Type: application/json' \
|
|
|
135
140
|
```
|
|
136
141
|
|
|
137
142
|
## Config (env)
|
|
138
|
-
`WINDHOVER_GRAPH` (module:attr
|
|
143
|
+
`WINDHOVER_GRAPH` (`module:attr`, or a comma list `name=module:attr,…`; unset = langgraph.json
|
|
144
|
+
discovery, else ingest-only) · `WINDHOVER_GRAPH_DIR` · `WINDHOVER_DB`
|
|
139
145
|
· `WINDHOVER_HOST`/`WINDHOVER_PORT` (0.0.0.0/8090) · `WINDHOVER_WATCH` (1) · `WINDHOVER_PRICING`
|
|
140
146
|
· `WINDHOVER_RETENTION_DAYS` (0 = keep forever; else prune older runs on startup + every 6h)
|
|
141
147
|
· `WINDHOVER_TOKEN` (set to require `Authorization: Bearer <token>` — or `?token=` — on all
|
|
142
|
-
`/api` routes; the UI prompts once and remembers it)
|
|
148
|
+
`/api` routes; the UI prompts once and remembers it)
|
|
149
|
+
· `WINDHOVER_WEBHOOK` (POST a JSON alert whenever a run errors or pauses on an interrupt).
|
|
143
150
|
Edit `windhover/pricing.json` for your models' $/1M rates (unknown model → cost null).
|
|
144
151
|
|
|
145
152
|
## Docs
|
|
@@ -98,6 +98,11 @@ graph.invoke(input, config={
|
|
|
98
98
|
- **Run history + replay** — SQLite; runs persist even if the browser closes (worker thread).
|
|
99
99
|
- **Living graph** — file watcher re-extracts topology in a subprocess and pushes it to the UI.
|
|
100
100
|
- **Dashboards** — runs/tokens per day, per-model usage and latency, per-node latency, error rate.
|
|
101
|
+
- **Multi-graph** — serve every graph in your project behind one URL; a top-bar selector
|
|
102
|
+
scopes all views (runs, sessions, stats included — metrics never mix graphs).
|
|
103
|
+
- **Alerts** — `WINDHOVER_WEBHOOK` pushes a JSON summary when a run errors or pauses.
|
|
104
|
+
- **Never slows your app** — the remote tracer is non-blocking (bounded queue; drops
|
|
105
|
+
rather than delays when the collector is down).
|
|
101
106
|
- **Mobile-first PWA**, light/dark. Fully local (FastAPI + Cytoscape.js).
|
|
102
107
|
|
|
103
108
|
## Datasets API
|
|
@@ -116,11 +121,13 @@ curl -X POST :8090/api/runs/RUN_ID/scores -H 'Content-Type: application/json' \
|
|
|
116
121
|
```
|
|
117
122
|
|
|
118
123
|
## Config (env)
|
|
119
|
-
`WINDHOVER_GRAPH` (module:attr
|
|
124
|
+
`WINDHOVER_GRAPH` (`module:attr`, or a comma list `name=module:attr,…`; unset = langgraph.json
|
|
125
|
+
discovery, else ingest-only) · `WINDHOVER_GRAPH_DIR` · `WINDHOVER_DB`
|
|
120
126
|
· `WINDHOVER_HOST`/`WINDHOVER_PORT` (0.0.0.0/8090) · `WINDHOVER_WATCH` (1) · `WINDHOVER_PRICING`
|
|
121
127
|
· `WINDHOVER_RETENTION_DAYS` (0 = keep forever; else prune older runs on startup + every 6h)
|
|
122
128
|
· `WINDHOVER_TOKEN` (set to require `Authorization: Bearer <token>` — or `?token=` — on all
|
|
123
|
-
`/api` routes; the UI prompts once and remembers it)
|
|
129
|
+
`/api` routes; the UI prompts once and remembers it)
|
|
130
|
+
· `WINDHOVER_WEBHOOK` (POST a JSON alert whenever a run errors or pauses on an interrupt).
|
|
124
131
|
Edit `windhover/pricing.json` for your models' $/1M rates (unknown model → cost null).
|
|
125
132
|
|
|
126
133
|
## Docs
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Windhover — engineering spec (current as of v0.
|
|
1
|
+
# Windhover — engineering spec (current as of v0.14)
|
|
2
2
|
|
|
3
3
|
Self-hosted LangGraph/LangChain observability + human-in-the-loop console.
|
|
4
4
|
Code is the only source of truth; Windhover observes and, for HITL, drives the
|
|
@@ -22,22 +22,36 @@ breakpoints, update_state, checkpoint forking). No visual editing, ever.
|
|
|
22
22
|
## Architecture
|
|
23
23
|
- **FastAPI + SSE** (no websockets). Single-file UI (`static/index.html`) +
|
|
24
24
|
vendored cytoscape/dagre/fonts.
|
|
25
|
+
- **Multi-graph registry**: `WINDHOVER_GRAPH` comma list (`name=module:attr`)
|
|
26
|
+
or every graph in `langgraph.json`; per-graph `Topo` watchers; `graph=`
|
|
27
|
+
scopes graph/schema/source/memory/threads/stats/sessions/dataset-run;
|
|
28
|
+
`/api/run` takes `_graph`; run rows store the registry name.
|
|
25
29
|
- **Topology**: subprocess (`windhover.extract`) re-imports the graph on file
|
|
26
30
|
mtime change → nodes/edges (+labels/metadata), input & context schemas,
|
|
27
31
|
per-node source (inspect, unwrapped), x-ray variant; sha1-hash change bumps a
|
|
28
32
|
version pushed over `/api/events` SSE.
|
|
29
|
-
- **Tracer** (`windhover.tracer.SpanBuilder`):
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
- **Tracer** (`windhover.tracer.SpanBuilder`): concurrency-safe per-root run
|
|
34
|
+
contexts (parallel invokes = separate runs; bare llm/tool/retriever calls
|
|
35
|
+
open implicit runs; bookkeeping purged per run — no growth in long-lived
|
|
36
|
+
apps). Captures chains→node spans, LLM (params, tools offered,
|
|
37
|
+
tokens+cache/reasoning details, TTFT, streaming partials ~0.5 s), tools,
|
|
38
|
+
retrievers, retries, custom events, GraphInterrupt→interrupted (a pause,
|
|
39
|
+
never an error), messages as {role, content, tool_calls},
|
|
40
|
+
session/tags/run_name/thread_id via config metadata. The HTTP sink is
|
|
41
|
+
non-blocking (bounded queue + drain thread, sheds oldest). Best-effort:
|
|
42
|
+
never raises into — or slows — the user's graph.
|
|
34
43
|
- **Store** (`windhover.store`, SQLite WAL, schema v7): runs / spans (tree) /
|
|
35
44
|
scores / datasets (+span_fts). Feature-detects FTS5 & json1 at startup;
|
|
36
45
|
idempotent column migrations.
|
|
37
46
|
- **HITL** (`/api/threads/*`): thin wrappers over `Command(resume|goto)`,
|
|
38
47
|
`interrupt_before/after`, `update_state`, `get_state_history`, checkpoint-id
|
|
39
|
-
forking. Requires the local graph to have a checkpointer.
|
|
48
|
+
forking. Requires the local graph to have a checkpointer. Pending-node
|
|
49
|
+
detection queries by thread only (a checkpoint_id would read history).
|
|
50
|
+
- **Cache visibility**: LangGraph `CachePolicy` hits fire no callbacks; local
|
|
51
|
+
runs synthesize the node span with `params.cached`.
|
|
40
52
|
- **Auth**: optional `WINDHOVER_TOKEN` Bearer/?token= gate on `/api` only.
|
|
53
|
+
- **Alerts**: `WINDHOVER_WEBHOOK` POSTs a summary on error/interrupted runs
|
|
54
|
+
(store.on_run_closed hook, fire-and-forget, deduped).
|
|
41
55
|
|
|
42
56
|
## Data model (schema v7)
|
|
43
57
|
`runs`: id · graph · source(ui|ingest) · status(running|done|error|interrupted) ·
|
|
@@ -154,6 +154,23 @@ curl -X POST :8090/api/datasets -H 'Content-Type: application/json' -d '{
|
|
|
154
154
|
numbers/booleans, substring-of-output-JSON for strings. It's a smoke-level matcher —
|
|
155
155
|
for semantic grading, run your own judge and `POST /api/runs/{id}/scores`.
|
|
156
156
|
|
|
157
|
+
## Deep agents & agent frameworks
|
|
158
|
+
|
|
159
|
+
Agent frameworks built on LangGraph — including `deepagents` (planning, virtual file
|
|
160
|
+
system, sub-agents) and `create_react_agent` — trace with **no integration work**:
|
|
161
|
+
|
|
162
|
+
- Every LLM call shows the **tools the model was offered**, so you can see an agent's
|
|
163
|
+
action space shrink inside a sub-agent.
|
|
164
|
+
- **Sub-agents nest**: the `task` tool call that spawns one carries the entire child
|
|
165
|
+
agent's spans beneath it — one tree from top-level plan to nested completion.
|
|
166
|
+
- **Plans render as checklists**: todo-shaped state (`[{content, status}]`) displays
|
|
167
|
+
with ✓/◐/○ markers instead of raw JSON, and state evolution shows each planning
|
|
168
|
+
update in order.
|
|
169
|
+
- Framework-internal middleware spans (e.g. `TodoListMiddleware.after_model`) appear
|
|
170
|
+
italic-muted so the agent's real work stands out.
|
|
171
|
+
- The canvas shows the compiled loop (agent ↔ tools) — an agent's *structure* lives in
|
|
172
|
+
its traces, not its topology; that's the nature of dynamic agents.
|
|
173
|
+
|
|
157
174
|
## More things the audit covered
|
|
158
175
|
|
|
159
176
|
- **Concurrency & batch** — one tracer instance safely handles parallel `.invoke()`s:
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "windhover"
|
|
7
|
-
version = "0.14.
|
|
7
|
+
version = "0.14.3"
|
|
8
8
|
description = "Self-hosted, mobile-friendly observability for LangGraph — traces, run history, cost, and a living graph view."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -330,6 +330,13 @@ window.pickDiff=async(id)=>{
|
|
|
330
330
|
.msg .mrole{flex:none;font-family:var(--mono);font-size:9.5px;letter-spacing:.08em;text-transform:uppercase;color:var(--muted);padding-top:2px;width:64px;text-align:right}
|
|
331
331
|
.msg.user .mrole{color:var(--accent-text)} .msg.assistant .mrole{color:var(--chip-llm-t)} .msg.tool .mrole,.msg.system .mrole{color:var(--chip-tool-t)}
|
|
332
332
|
.msg>div{background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:5px 9px;white-space:pre-wrap;word-break:break-word;flex:1}
|
|
333
|
+
.todos{background:var(--surface2);border:1px solid var(--border);border-radius:8px;padding:8px 12px;display:flex;flex-direction:column;gap:4px}
|
|
334
|
+
.todo{font-size:12.5px;display:flex;gap:8px;align-items:baseline}
|
|
335
|
+
.todo .ti{font-family:var(--mono);color:var(--muted)}
|
|
336
|
+
.todo.completed{color:var(--muted);text-decoration:line-through}
|
|
337
|
+
.todo.completed .ti{color:var(--ok);text-decoration:none}
|
|
338
|
+
.todo.in_progress .ti{color:var(--warn)}
|
|
339
|
+
.tr-name.fw{color:var(--muted);font-weight:500;font-style:italic}
|
|
333
340
|
|
|
334
341
|
/* ---------- modal (new run) ---------- */
|
|
335
342
|
.modal{position:fixed;inset:0;display:grid;place-items:center;z-index:60;opacity:0;pointer-events:none;transition:.15s}
|
|
@@ -1008,7 +1015,7 @@ function renderSpans(list,byParent,total,depth){
|
|
|
1008
1015
|
<div class="tr-row" onclick="tog('${did}',this)">
|
|
1009
1016
|
<div class="tr-main"><span class="indent" style="width:${depth*18}px"></span>
|
|
1010
1017
|
<span class="caret">▶</span><span class="chip ${s.type}">${s.type}</span>
|
|
1011
|
-
<span class="tr-name ${s.status==='error'?'err':''}">${esc(s.name||s.type)}</span>${s.retries?`<span class="badge-retry">↻${s.retries}</span>`:''}</div>
|
|
1018
|
+
<span class="tr-name ${s.status==='error'?'err':''} ${/Middleware\./.test(s.name||'')?'fw':''}">${esc(s.name||s.type)}</span>${s.retries?`<span class="badge-retry">↻${s.retries}</span>`:''}</div>
|
|
1012
1019
|
<div class="tr-gantt"><div class="tr-bar ${s.type}" style="left:${left}%;width:${width}%"></div></div>
|
|
1013
1020
|
<div class="tr-meta">${meta}</div>
|
|
1014
1021
|
</div>
|
|
@@ -1024,6 +1031,9 @@ function renderSpans(list,byParent,total,depth){
|
|
|
1024
1031
|
}).join('');
|
|
1025
1032
|
}
|
|
1026
1033
|
function isMsg(m){ return m&&typeof m==='object'&&typeof m.content!=='undefined'&&typeof (m.role||m.type)==='string'; }
|
|
1034
|
+
function isTodo(x){ return x&&typeof x==='object'&&typeof x.content==='string'&&typeof x.status==='string'; }
|
|
1035
|
+
function todoHtml(arr){ const ic={completed:'✓','in_progress':'◐',pending:'○'};
|
|
1036
|
+
return `<div class="todos">${arr.map(t=>`<div class="todo ${esc(t.status)}"><span class="ti">${ic[t.status]||'○'}</span>${esc(t.content)}</div>`).join('')}</div>`; }
|
|
1027
1037
|
function chatHtml(arr){ return `<div class="chat">${arr.map(m=>{
|
|
1028
1038
|
const role=(m.role||m.type||'').replace('human','user').replace('ai','assistant');
|
|
1029
1039
|
const body=typeof m.content==='string'?m.content:JSON.stringify(m.content);
|
|
@@ -1033,8 +1043,10 @@ function chatHtml(arr){ return `<div class="chat">${arr.map(m=>{
|
|
|
1033
1043
|
function kv(k,v){
|
|
1034
1044
|
const msgs=Array.isArray(v)&&v.length&&v.every(isMsg)?v
|
|
1035
1045
|
:(v&&typeof v==='object'&&Array.isArray(v.messages)&&v.messages.length&&v.messages.every(isMsg)?v.messages:null);
|
|
1046
|
+
const todos=Array.isArray(v)&&v.length&&v.every(isTodo)?v
|
|
1047
|
+
:(v&&typeof v==='object'&&Array.isArray(v.todos)&&v.todos.length&&v.todos.every(isTodo)?v.todos:null);
|
|
1036
1048
|
const txt=typeof v==='string'?v:JSON.stringify(v,null,2);
|
|
1037
|
-
return `<div class="kv"><div class="k">${k}<button class="copy" onclick="navigator.clipboard.writeText(${JSON.stringify(txt).replace(/"/g,'"')});toast('Copied')">copy</button></div>${msgs?chatHtml(msgs):`<pre>${esc(txt)}</pre>`}</div>`; }
|
|
1049
|
+
return `<div class="kv"><div class="k">${k}<button class="copy" onclick="navigator.clipboard.writeText(${JSON.stringify(txt).replace(/"/g,'"')});toast('Copied')">copy</button></div>${msgs?chatHtml(msgs):(todos?todoHtml(todos):`<pre>${esc(txt)}</pre>`)}</div>`; }
|
|
1038
1050
|
window.tog=(id,row)=>{ const el=document.getElementById(id); const open=el.style.display==='block';
|
|
1039
1051
|
el.style.display=open?'none':'block'; row.querySelector('.caret').classList.toggle('open',!open); };
|
|
1040
1052
|
window.replay=async(steps)=>{ closeAll(); switchView('graph');
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{windhover-0.14.2 → windhover-0.14.3}/windhover/static/vendor/fonts/fraunces-600-italic.woff2
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|