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.
Files changed (35) hide show
  1. {windhover-0.14.2 → windhover-0.14.3}/PKG-INFO +10 -3
  2. {windhover-0.14.2 → windhover-0.14.3}/README.md +9 -2
  3. {windhover-0.14.2 → windhover-0.14.3}/SPEC.md +21 -7
  4. {windhover-0.14.2 → windhover-0.14.3}/docs/GUIDE.md +17 -0
  5. {windhover-0.14.2 → windhover-0.14.3}/pyproject.toml +1 -1
  6. {windhover-0.14.2 → windhover-0.14.3}/windhover/static/index.html +14 -2
  7. {windhover-0.14.2 → windhover-0.14.3}/.github/workflows/ci.yml +0 -0
  8. {windhover-0.14.2 → windhover-0.14.3}/.gitignore +0 -0
  9. {windhover-0.14.2 → windhover-0.14.3}/GUIDE.md +0 -0
  10. {windhover-0.14.2 → windhover-0.14.3}/LICENSE +0 -0
  11. {windhover-0.14.2 → windhover-0.14.3}/docs/graph.png +0 -0
  12. {windhover-0.14.2 → windhover-0.14.3}/docs/logo.svg +0 -0
  13. {windhover-0.14.2 → windhover-0.14.3}/docs/runs.png +0 -0
  14. {windhover-0.14.2 → windhover-0.14.3}/docs/social-preview.png +0 -0
  15. {windhover-0.14.2 → windhover-0.14.3}/docs/stats.png +0 -0
  16. {windhover-0.14.2 → windhover-0.14.3}/docs/trace.png +0 -0
  17. {windhover-0.14.2 → windhover-0.14.3}/tests/__init__.py +0 -0
  18. {windhover-0.14.2 → windhover-0.14.3}/tests/test_smoke.py +0 -0
  19. {windhover-0.14.2 → windhover-0.14.3}/windhover/__init__.py +0 -0
  20. {windhover-0.14.2 → windhover-0.14.3}/windhover/config.py +0 -0
  21. {windhover-0.14.2 → windhover-0.14.3}/windhover/demo_graph.py +0 -0
  22. {windhover-0.14.2 → windhover-0.14.3}/windhover/demo_rag.py +0 -0
  23. {windhover-0.14.2 → windhover-0.14.3}/windhover/extract.py +0 -0
  24. {windhover-0.14.2 → windhover-0.14.3}/windhover/pricing.json +0 -0
  25. {windhover-0.14.2 → windhover-0.14.3}/windhover/server.py +0 -0
  26. {windhover-0.14.2 → windhover-0.14.3}/windhover/static/icon.svg +0 -0
  27. {windhover-0.14.2 → windhover-0.14.3}/windhover/static/manifest.json +0 -0
  28. {windhover-0.14.2 → windhover-0.14.3}/windhover/static/vendor/cytoscape-dagre.js +0 -0
  29. {windhover-0.14.2 → windhover-0.14.3}/windhover/static/vendor/cytoscape.min.js +0 -0
  30. {windhover-0.14.2 → windhover-0.14.3}/windhover/static/vendor/dagre.min.js +0 -0
  31. {windhover-0.14.2 → windhover-0.14.3}/windhover/static/vendor/fonts/fraunces-500.woff2 +0 -0
  32. {windhover-0.14.2 → windhover-0.14.3}/windhover/static/vendor/fonts/fraunces-600-italic.woff2 +0 -0
  33. {windhover-0.14.2 → windhover-0.14.3}/windhover/static/vendor/fonts/fraunces-600.woff2 +0 -0
  34. {windhover-0.14.2 → windhover-0.14.3}/windhover/store.py +0 -0
  35. {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.2
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; unset = ingest-only) · `WINDHOVER_GRAPH_DIR` · `WINDHOVER_DB`
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; unset = ingest-only) · `WINDHOVER_GRAPH_DIR` · `WINDHOVER_DB`
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.11)
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`): chains→node spans (langgraph_node
30
- metadata), LLM (params, tools offered, tokens+details, TTFT, streaming
31
- partials every ~0.5 s), tools, retrievers, retries, custom events,
32
- GraphInterruptinterrupted, session/tags/run_name/thread_id via config
33
- metadata. Best-effort: never raises into the user's graph.
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 chainsnode 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.2"
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,'&quot;')});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,'&quot;')});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