Repository files navigation

loopscope

Python 3.10+LangGraphStatusLocalLicense

A local live dashboard for LangGraph state graphs and Ralph loops (retry-until-done: plan / edit / test until a number hits zero).

Your agent is already running. loopscope sits in the same process, opens http://127.0.0.1:7788, and shows the run as it happens: which node is hot, how the graph is wired, whether this pass is better than the last, and a colour-coded feed of tools and models. One hook. No collector, no database, no accounts.

It does not rewrite the graph. It does not wrap your nodes. Forget the hook and the agent still runs — the dashboard just stays empty.

0.1 Beta. Loopback only; there is no auth.

loopscope dashboard

Quick start

git clone https://github.com/sw30labs/loopscope.git
cd loopscope
./setup_and_run.sh # venv + tests + Ralph demo → http://localhost:7788
./setup_and_run.sh --langgraph # cyclic graph
./setup_and_run.sh --combo # Ralph loop driving the graph
./setup_and_run.sh --help

Or by hand:

python -m venv .venv &&source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt
python examples/ralph_demo.py # → http://localhost:7788

The Ralph demo needs nothing but FastAPI, uvicorn, and websockets. For LangGraph:

pip install langgraph
python examples/langgraph_demo.py # one graph run
python examples/langgraph_demo.py ralph # a Ralph loop driving the graph

A LangGraph run

Installing the package itself is optional — the examples add the repo root to sys.path when run from here. To use it in your own project:

pip install -e .

Hook it into a graph you already have

QUICKSTART-INTEGRATION.md is the hand recipe. In VS Code / Grok, from that project: /loopscope-hook.

importloopscopescope=loopscope.start(open_browser=True)
config=loopscope.attach(app) # returns a LangChain configtry:
app.invoke(state, config=config) # .ainvoke / .stream / .astream all workloopscope.finish(config)
exceptException:
loopscope.finish(config, status="error")
raisescope.hold() # keep the dashboard up after the script ends

attach() publishes the topology and appends a callback handler to whatever config you pass in. Each top-level invocation counts as one pass, so a plain while loop around app.invoke() already fills the tape.

Ralph loops

loop=loopscope.RalphLoop("get the suite green",
phases=["plan", "edit", "test", "review"],
max_iters=20, stall_after=5,
roles={"test": "run the suite"}) # subtitlesforitinloop:
withit.phase("plan") asp:
p.log("picking the two cheapest failures")
withit.phase("edit"):
apply_patch()
withit.phase("test") asp:
failures=run_tests()
p.log(f"{failures} failing")
it.signal(failures, name="failing tests") # the number to drive to zeroit.note(f"{failures} failing")
iffailures==0:
it.done("suite green")

it.done() sets a flag rather than raising — an exception thrown in a for body never reaches the generator that yielded it, so raising would skip the loop's own bookkeeping. The rest of the pass still runs; break right after if you want out immediately.

The loop stops on done(), max_iters, max_seconds, or stall: when signal has not improved for stall_after passes it says so and quits, which is the failure mode a Ralph loop actually has.

Decorator form:

@loopscope.ralph("keep fixing the build", phases=["edit", "test"])defone_pass(it):
...
returnfailures# falsy return converges the loop

Both at once

A Ralph loop outside, a graph inside, one timeline — graph nodes on the left, Ralph passes on the tape:

loop=loopscope.RalphLoop("draft until it scores")
config=loop.attach_graph(app)
foritinloop:
state=app.invoke(state, config=config)
it.signal(1-state["score"], name="distance to 1.0")
ifstate["score"] >=0.9:
it.done("good enough")

Record and replay

loopscope.start(jsonl="runs/tonight.jsonl")
python -m loopscope.replay runs/tonight.jsonl --speed 8

Overnight loops finish while you are asleep; the replay plays back at whatever speed you can stand.

Any loop at all can push to the same dashboard:

loopscope.log("compaction finished", level="warn")
loopscope.metric("open_todos", 14)

What you see

A Ralph loop mid-run

The mesh — your nodes as a constellation. The busiest node (a supervisor, an orchestrator, whatever everything routes through) becomes the hub; the rest orbit it. A plain cycle has no such node, so a Ralph loop puts its objective and pass counter in the middle instead. Each node carries a ring showing its share of the run's time, a status pill, and a subtitle taken from its docstring. When control moves, a dot rides the link it moved along.

Stage stepper — the graph's layers as numbered stages, with the one currently executing lit.

Pipeline / Queue / Node status — left rail. Time share per node, then a compact pass counter (left, running, done, revisits, errors — it does not grow), then each node's hit count. LangGraph runs do not enqueue a long work list here.

Passes — one column per pass, one row per node. Skipped nodes leave gaps, revisits show a count, and the bar under each column is your convergence signal, coloured red at the worst value seen and green at zero. Twenty passes read as a fabric: convergence is a staircase, thrashing is noise. Single-pass runs get a throughput chart here instead. Throughput and System sit as an equal pair under the mesh.

Live feed — timestamped node, tool and model lines, colour-coded per node.

System — node runs, average and slowest, edges exercised, error rate, and why the run stopped.

How it fits together

your code ──► EventBus ──► websocket ──► dashboard
(ring buffer, thread-safe, optional JSONL)
  • The server runs on its own thread with its own event loop, so start() returns immediately and publishing is safe from any thread — sync .invoke() on the main thread, an async app on its own loop, a worker pool.
  • The ring buffer (4000 events; topology is pinned and never evicted) is replayed to every new browser. Opening the tab halfway through a run still draws the graph and recent history. Overnight loops that wrap the buffer lose early log lines — pass jsonl= if you need the full tape.
  • Layout (layering, cycle-breaking, hub detection, orbit placement, colour assignment) is computed in Python; the browser draws coordinates and stretches the orbit to whatever aspect the pane has.
  • raise_error = False on the handler and swallowed exceptions in publish(): a broken dashboard must never take down the run it is watching.

Default bind is 127.0.0.1:7788. Change it with loopscope.start(port=...).

Notes and limits

  • Node subtitles come from the first line of each node function's docstring, or from roles={"node": "text"} if you would rather say it explicitly.
  • Node identity comes from metadata["langgraph_node"], filtered to runs tagged graph:step:N. Without that filter, routers and chains inside a node double-count as node executions.
  • Edge traversal is inferred from execution order, which is exact for sequential graphs and approximate under parallel fan-out.
  • State deltas are truncated for the wire (600 chars, 40 keys, 12 list items). Obvious secret-shaped keys are redacted. The dashboard is for watching, not for auditing.
  • Python 3.10 or newer.
  • Bind to 127.0.0.1 unless you mean it; there is no auth. Binding 0.0.0.0 prints a warning. The websocket accepts same-origin browsers and loopback clients only.

License

Apache-2.0. See LICENSE.

About

Local live dashboard for LangGraph graphs and Ralph loops. Apache 2.0.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

loopscope

Python 3.10+LangGraphStatusLocalLicense

A local live dashboard for LangGraph state graphs and Ralph loops (retry-until-done: plan / edit / test until a number hits zero).

Your agent is already running. loopscope sits in the same process, opens http://127.0.0.1:7788, and shows the run as it happens: which node is hot, how the graph is wired, whether this pass is better than the last, and a colour-coded feed of tools and models. One hook. No collector, no database, no accounts.

It does not rewrite the graph. It does not wrap your nodes. Forget the hook and the agent still runs — the dashboard just stays empty.

0.1 Beta. Loopback only; there is no auth.

loopscope dashboard

Quick start

git clone https://github.com/sw30labs/loopscope.git
cd loopscope
./setup_and_run.sh # venv + tests + Ralph demo → http://localhost:7788
./setup_and_run.sh --langgraph # cyclic graph
./setup_and_run.sh --combo # Ralph loop driving the graph
./setup_and_run.sh --help

Or by hand:

python -m venv .venv &&source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt
python examples/ralph_demo.py # → http://localhost:7788

The Ralph demo needs nothing but FastAPI, uvicorn, and websockets. For LangGraph:

pip install langgraph
python examples/langgraph_demo.py # one graph run
python examples/langgraph_demo.py ralph # a Ralph loop driving the graph

A LangGraph run

Installing the package itself is optional — the examples add the repo root to sys.path when run from here. To use it in your own project:

pip install -e .

Hook it into a graph you already have

QUICKSTART-INTEGRATION.md is the hand recipe. In VS Code / Grok, from that project: /loopscope-hook.

importloopscopescope=loopscope.start(open_browser=True)
config=loopscope.attach(app) # returns a LangChain configtry:
app.invoke(state, config=config) # .ainvoke / .stream / .astream all workloopscope.finish(config)
exceptException:
loopscope.finish(config, status="error")
raisescope.hold() # keep the dashboard up after the script ends

attach() publishes the topology and appends a callback handler to whatever config you pass in. Each top-level invocation counts as one pass, so a plain while loop around app.invoke() already fills the tape.

Ralph loops

loop=loopscope.RalphLoop("get the suite green",
phases=["plan", "edit", "test", "review"],
max_iters=20, stall_after=5,
roles={"test": "run the suite"}) # subtitlesforitinloop:
withit.phase("plan") asp:
p.log("picking the two cheapest failures")
withit.phase("edit"):
apply_patch()
withit.phase("test") asp:
failures=run_tests()
p.log(f"{failures} failing")
it.signal(failures, name="failing tests") # the number to drive to zeroit.note(f"{failures} failing")
iffailures==0:
it.done("suite green")

it.done() sets a flag rather than raising — an exception thrown in a for body never reaches the generator that yielded it, so raising would skip the loop's own bookkeeping. The rest of the pass still runs; break right after if you want out immediately.

The loop stops on done(), max_iters, max_seconds, or stall: when signal has not improved for stall_after passes it says so and quits, which is the failure mode a Ralph loop actually has.

Decorator form:

@loopscope.ralph("keep fixing the build", phases=["edit", "test"])defone_pass(it):
...
returnfailures# falsy return converges the loop

Both at once

A Ralph loop outside, a graph inside, one timeline — graph nodes on the left, Ralph passes on the tape:

loop=loopscope.RalphLoop("draft until it scores")
config=loop.attach_graph(app)
foritinloop:
state=app.invoke(state, config=config)
it.signal(1-state["score"], name="distance to 1.0")
ifstate["score"] >=0.9:
it.done("good enough")

Record and replay

loopscope.start(jsonl="runs/tonight.jsonl")
python -m loopscope.replay runs/tonight.jsonl --speed 8

Overnight loops finish while you are asleep; the replay plays back at whatever speed you can stand.

Any loop at all can push to the same dashboard:

loopscope.log("compaction finished", level="warn")
loopscope.metric("open_todos", 14)

What you see

A Ralph loop mid-run

The mesh — your nodes as a constellation. The busiest node (a supervisor, an orchestrator, whatever everything routes through) becomes the hub; the rest orbit it. A plain cycle has no such node, so a Ralph loop puts its objective and pass counter in the middle instead. Each node carries a ring showing its share of the run's time, a status pill, and a subtitle taken from its docstring. When control moves, a dot rides the link it moved along.

Stage stepper — the graph's layers as numbered stages, with the one currently executing lit.

Pipeline / Queue / Node status — left rail. Time share per node, then a compact pass counter (left, running, done, revisits, errors — it does not grow), then each node's hit count. LangGraph runs do not enqueue a long work list here.

Passes — one column per pass, one row per node. Skipped nodes leave gaps, revisits show a count, and the bar under each column is your convergence signal, coloured red at the worst value seen and green at zero. Twenty passes read as a fabric: convergence is a staircase, thrashing is noise. Single-pass runs get a throughput chart here instead. Throughput and System sit as an equal pair under the mesh.

Live feed — timestamped node, tool and model lines, colour-coded per node.

System — node runs, average and slowest, edges exercised, error rate, and why the run stopped.

How it fits together

your code ──► EventBus ──► websocket ──► dashboard
(ring buffer, thread-safe, optional JSONL)
  • The server runs on its own thread with its own event loop, so start() returns immediately and publishing is safe from any thread — sync .invoke() on the main thread, an async app on its own loop, a worker pool.
  • The ring buffer (4000 events; topology is pinned and never evicted) is replayed to every new browser. Opening the tab halfway through a run still draws the graph and recent history. Overnight loops that wrap the buffer lose early log lines — pass jsonl= if you need the full tape.
  • Layout (layering, cycle-breaking, hub detection, orbit placement, colour assignment) is computed in Python; the browser draws coordinates and stretches the orbit to whatever aspect the pane has.
  • raise_error = False on the handler and swallowed exceptions in publish(): a broken dashboard must never take down the run it is watching.

Default bind is 127.0.0.1:7788. Change it with loopscope.start(port=...).

Notes and limits

  • Node subtitles come from the first line of each node function's docstring, or from roles={"node": "text"} if you would rather say it explicitly.
  • Node identity comes from metadata["langgraph_node"], filtered to runs tagged graph:step:N. Without that filter, routers and chains inside a node double-count as node executions.
  • Edge traversal is inferred from execution order, which is exact for sequential graphs and approximate under parallel fan-out.
  • State deltas are truncated for the wire (600 chars, 40 keys, 12 list items). Obvious secret-shaped keys are redacted. The dashboard is for watching, not for auditing.
  • Python 3.10 or newer.
  • Bind to 127.0.0.1 unless you mean it; there is no auth. Binding 0.0.0.0 prints a warning. The websocket accepts same-origin browsers and loopback clients only.

License

Apache-2.0. See LICENSE.

About

Local live dashboard for LangGraph graphs and Ralph loops. Apache 2.0.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

loopscope

Python 3.10+LangGraphStatusLocalLicense

A local live dashboard for LangGraph state graphs and Ralph loops (retry-until-done: plan / edit / test until a number hits zero).

Your agent is already running. loopscope sits in the same process, opens http://127.0.0.1:7788, and shows the run as it happens: which node is hot, how the graph is wired, whether this pass is better than the last, and a colour-coded feed of tools and models. One hook. No collector, no database, no accounts.

It does not rewrite the graph. It does not wrap your nodes. Forget the hook and the agent still runs — the dashboard just stays empty.

0.1 Beta. Loopback only; there is no auth.

loopscope dashboard

Quick start

git clone https://github.com/sw30labs/loopscope.git
cd loopscope
./setup_and_run.sh # venv + tests + Ralph demo → http://localhost:7788
./setup_and_run.sh --langgraph # cyclic graph
./setup_and_run.sh --combo # Ralph loop driving the graph
./setup_and_run.sh --help

Or by hand:

python -m venv .venv &&source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt
python examples/ralph_demo.py # → http://localhost:7788

The Ralph demo needs nothing but FastAPI, uvicorn, and websockets. For LangGraph:

pip install langgraph
python examples/langgraph_demo.py # one graph run
python examples/langgraph_demo.py ralph # a Ralph loop driving the graph

A LangGraph run

Installing the package itself is optional — the examples add the repo root to sys.path when run from here. To use it in your own project:

pip install -e .

Hook it into a graph you already have

QUICKSTART-INTEGRATION.md is the hand recipe. In VS Code / Grok, from that project: /loopscope-hook.

importloopscopescope=loopscope.start(open_browser=True)
config=loopscope.attach(app) # returns a LangChain configtry:
app.invoke(state, config=config) # .ainvoke / .stream / .astream all workloopscope.finish(config)
exceptException:
loopscope.finish(config, status="error")
raisescope.hold() # keep the dashboard up after the script ends

attach() publishes the topology and appends a callback handler to whatever config you pass in. Each top-level invocation counts as one pass, so a plain while loop around app.invoke() already fills the tape.

Ralph loops

loop=loopscope.RalphLoop("get the suite green",
phases=["plan", "edit", "test", "review"],
max_iters=20, stall_after=5,
roles={"test": "run the suite"}) # subtitlesforitinloop:
withit.phase("plan") asp:
p.log("picking the two cheapest failures")
withit.phase("edit"):
apply_patch()
withit.phase("test") asp:
failures=run_tests()
p.log(f"{failures} failing")
it.signal(failures, name="failing tests") # the number to drive to zeroit.note(f"{failures} failing")
iffailures==0:
it.done("suite green")

it.done() sets a flag rather than raising — an exception thrown in a for body never reaches the generator that yielded it, so raising would skip the loop's own bookkeeping. The rest of the pass still runs; break right after if you want out immediately.

The loop stops on done(), max_iters, max_seconds, or stall: when signal has not improved for stall_after passes it says so and quits, which is the failure mode a Ralph loop actually has.

Decorator form:

@loopscope.ralph("keep fixing the build", phases=["edit", "test"])defone_pass(it):
...
returnfailures# falsy return converges the loop

Both at once

A Ralph loop outside, a graph inside, one timeline — graph nodes on the left, Ralph passes on the tape:

loop=loopscope.RalphLoop("draft until it scores")
config=loop.attach_graph(app)
foritinloop:
state=app.invoke(state, config=config)
it.signal(1-state["score"], name="distance to 1.0")
ifstate["score"] >=0.9:
it.done("good enough")

Record and replay

loopscope.start(jsonl="runs/tonight.jsonl")
python -m loopscope.replay runs/tonight.jsonl --speed 8

Overnight loops finish while you are asleep; the replay plays back at whatever speed you can stand.

Any loop at all can push to the same dashboard:

loopscope.log("compaction finished", level="warn")
loopscope.metric("open_todos", 14)

What you see

A Ralph loop mid-run

The mesh — your nodes as a constellation. The busiest node (a supervisor, an orchestrator, whatever everything routes through) becomes the hub; the rest orbit it. A plain cycle has no such node, so a Ralph loop puts its objective and pass counter in the middle instead. Each node carries a ring showing its share of the run's time, a status pill, and a subtitle taken from its docstring. When control moves, a dot rides the link it moved along.

Stage stepper — the graph's layers as numbered stages, with the one currently executing lit.

Pipeline / Queue / Node status — left rail. Time share per node, then a compact pass counter (left, running, done, revisits, errors — it does not grow), then each node's hit count. LangGraph runs do not enqueue a long work list here.

Passes — one column per pass, one row per node. Skipped nodes leave gaps, revisits show a count, and the bar under each column is your convergence signal, coloured red at the worst value seen and green at zero. Twenty passes read as a fabric: convergence is a staircase, thrashing is noise. Single-pass runs get a throughput chart here instead. Throughput and System sit as an equal pair under the mesh.

Live feed — timestamped node, tool and model lines, colour-coded per node.

System — node runs, average and slowest, edges exercised, error rate, and why the run stopped.

How it fits together

your code ──► EventBus ──► websocket ──► dashboard
(ring buffer, thread-safe, optional JSONL)
  • The server runs on its own thread with its own event loop, so start() returns immediately and publishing is safe from any thread — sync .invoke() on the main thread, an async app on its own loop, a worker pool.
  • The ring buffer (4000 events; topology is pinned and never evicted) is replayed to every new browser. Opening the tab halfway through a run still draws the graph and recent history. Overnight loops that wrap the buffer lose early log lines — pass jsonl= if you need the full tape.
  • Layout (layering, cycle-breaking, hub detection, orbit placement, colour assignment) is computed in Python; the browser draws coordinates and stretches the orbit to whatever aspect the pane has.
  • raise_error = False on the handler and swallowed exceptions in publish(): a broken dashboard must never take down the run it is watching.

Default bind is 127.0.0.1:7788. Change it with loopscope.start(port=...).

Notes and limits

  • Node subtitles come from the first line of each node function's docstring, or from roles={"node": "text"} if you would rather say it explicitly.
  • Node identity comes from metadata["langgraph_node"], filtered to runs tagged graph:step:N. Without that filter, routers and chains inside a node double-count as node executions.
  • Edge traversal is inferred from execution order, which is exact for sequential graphs and approximate under parallel fan-out.
  • State deltas are truncated for the wire (600 chars, 40 keys, 12 list items). Obvious secret-shaped keys are redacted. The dashboard is for watching, not for auditing.
  • Python 3.10 or newer.
  • Bind to 127.0.0.1 unless you mean it; there is no auth. Binding 0.0.0.0 prints a warning. The websocket accepts same-origin browsers and loopback clients only.

License

Apache-2.0. See LICENSE.

About

Local live dashboard for LangGraph graphs and Ralph loops. Apache 2.0.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

loopscope

Python 3.10+LangGraphStatusLocalLicense

A local live dashboard for LangGraph state graphs and Ralph loops (retry-until-done: plan / edit / test until a number hits zero).

Your agent is already running. loopscope sits in the same process, opens http://127.0.0.1:7788, and shows the run as it happens: which node is hot, how the graph is wired, whether this pass is better than the last, and a colour-coded feed of tools and models. One hook. No collector, no database, no accounts.

It does not rewrite the graph. It does not wrap your nodes. Forget the hook and the agent still runs — the dashboard just stays empty.

0.1 Beta. Loopback only; there is no auth.

loopscope dashboard

Quick start

git clone https://github.com/sw30labs/loopscope.git
cd loopscope
./setup_and_run.sh # venv + tests + Ralph demo → http://localhost:7788
./setup_and_run.sh --langgraph # cyclic graph
./setup_and_run.sh --combo # Ralph loop driving the graph
./setup_and_run.sh --help

Or by hand:

python -m venv .venv &&source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt
python examples/ralph_demo.py # → http://localhost:7788

The Ralph demo needs nothing but FastAPI, uvicorn, and websockets. For LangGraph:

pip install langgraph
python examples/langgraph_demo.py # one graph run
python examples/langgraph_demo.py ralph # a Ralph loop driving the graph

A LangGraph run

Installing the package itself is optional — the examples add the repo root to sys.path when run from here. To use it in your own project:

pip install -e .

Hook it into a graph you already have

QUICKSTART-INTEGRATION.md is the hand recipe. In VS Code / Grok, from that project: /loopscope-hook.

importloopscopescope=loopscope.start(open_browser=True)
config=loopscope.attach(app) # returns a LangChain configtry:
app.invoke(state, config=config) # .ainvoke / .stream / .astream all workloopscope.finish(config)
exceptException:
loopscope.finish(config, status="error")
raisescope.hold() # keep the dashboard up after the script ends

attach() publishes the topology and appends a callback handler to whatever config you pass in. Each top-level invocation counts as one pass, so a plain while loop around app.invoke() already fills the tape.

Ralph loops

loop=loopscope.RalphLoop("get the suite green",
phases=["plan", "edit", "test", "review"],
max_iters=20, stall_after=5,
roles={"test": "run the suite"}) # subtitlesforitinloop:
withit.phase("plan") asp:
p.log("picking the two cheapest failures")
withit.phase("edit"):
apply_patch()
withit.phase("test") asp:
failures=run_tests()
p.log(f"{failures} failing")
it.signal(failures, name="failing tests") # the number to drive to zeroit.note(f"{failures} failing")
iffailures==0:
it.done("suite green")

it.done() sets a flag rather than raising — an exception thrown in a for body never reaches the generator that yielded it, so raising would skip the loop's own bookkeeping. The rest of the pass still runs; break right after if you want out immediately.

The loop stops on done(), max_iters, max_seconds, or stall: when signal has not improved for stall_after passes it says so and quits, which is the failure mode a Ralph loop actually has.

Decorator form:

@loopscope.ralph("keep fixing the build", phases=["edit", "test"])defone_pass(it):
...
returnfailures# falsy return converges the loop

Both at once

A Ralph loop outside, a graph inside, one timeline — graph nodes on the left, Ralph passes on the tape:

loop=loopscope.RalphLoop("draft until it scores")
config=loop.attach_graph(app)
foritinloop:
state=app.invoke(state, config=config)
it.signal(1-state["score"], name="distance to 1.0")
ifstate["score"] >=0.9:
it.done("good enough")

Record and replay

loopscope.start(jsonl="runs/tonight.jsonl")
python -m loopscope.replay runs/tonight.jsonl --speed 8

Overnight loops finish while you are asleep; the replay plays back at whatever speed you can stand.

Any loop at all can push to the same dashboard:

loopscope.log("compaction finished", level="warn")
loopscope.metric("open_todos", 14)

What you see

A Ralph loop mid-run

The mesh — your nodes as a constellation. The busiest node (a supervisor, an orchestrator, whatever everything routes through) becomes the hub; the rest orbit it. A plain cycle has no such node, so a Ralph loop puts its objective and pass counter in the middle instead. Each node carries a ring showing its share of the run's time, a status pill, and a subtitle taken from its docstring. When control moves, a dot rides the link it moved along.

Stage stepper — the graph's layers as numbered stages, with the one currently executing lit.

Pipeline / Queue / Node status — left rail. Time share per node, then a compact pass counter (left, running, done, revisits, errors — it does not grow), then each node's hit count. LangGraph runs do not enqueue a long work list here.

Passes — one column per pass, one row per node. Skipped nodes leave gaps, revisits show a count, and the bar under each column is your convergence signal, coloured red at the worst value seen and green at zero. Twenty passes read as a fabric: convergence is a staircase, thrashing is noise. Single-pass runs get a throughput chart here instead. Throughput and System sit as an equal pair under the mesh.

Live feed — timestamped node, tool and model lines, colour-coded per node.

System — node runs, average and slowest, edges exercised, error rate, and why the run stopped.

How it fits together

your code ──► EventBus ──► websocket ──► dashboard
(ring buffer, thread-safe, optional JSONL)
  • The server runs on its own thread with its own event loop, so start() returns immediately and publishing is safe from any thread — sync .invoke() on the main thread, an async app on its own loop, a worker pool.
  • The ring buffer (4000 events; topology is pinned and never evicted) is replayed to every new browser. Opening the tab halfway through a run still draws the graph and recent history. Overnight loops that wrap the buffer lose early log lines — pass jsonl= if you need the full tape.
  • Layout (layering, cycle-breaking, hub detection, orbit placement, colour assignment) is computed in Python; the browser draws coordinates and stretches the orbit to whatever aspect the pane has.
  • raise_error = False on the handler and swallowed exceptions in publish(): a broken dashboard must never take down the run it is watching.

Default bind is 127.0.0.1:7788. Change it with loopscope.start(port=...).

Notes and limits

  • Node subtitles come from the first line of each node function's docstring, or from roles={"node": "text"} if you would rather say it explicitly.
  • Node identity comes from metadata["langgraph_node"], filtered to runs tagged graph:step:N. Without that filter, routers and chains inside a node double-count as node executions.
  • Edge traversal is inferred from execution order, which is exact for sequential graphs and approximate under parallel fan-out.
  • State deltas are truncated for the wire (600 chars, 40 keys, 12 list items). Obvious secret-shaped keys are redacted. The dashboard is for watching, not for auditing.
  • Python 3.10 or newer.
  • Bind to 127.0.0.1 unless you mean it; there is no auth. Binding 0.0.0.0 prints a warning. The websocket accepts same-origin browsers and loopback clients only.

License

Apache-2.0. See LICENSE.

About

Local live dashboard for LangGraph graphs and Ralph loops. Apache 2.0.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

loopscope

Python 3.10+LangGraphStatusLocalLicense

A local live dashboard for LangGraph state graphs and Ralph loops (retry-until-done: plan / edit / test until a number hits zero).

Your agent is already running. loopscope sits in the same process, opens http://127.0.0.1:7788, and shows the run as it happens: which node is hot, how the graph is wired, whether this pass is better than the last, and a colour-coded feed of tools and models. One hook. No collector, no database, no accounts.

It does not rewrite the graph. It does not wrap your nodes. Forget the hook and the agent still runs — the dashboard just stays empty.

0.1 Beta. Loopback only; there is no auth.

loopscope dashboard

Quick start

git clone https://github.com/sw30labs/loopscope.git
cd loopscope
./setup_and_run.sh # venv + tests + Ralph demo → http://localhost:7788
./setup_and_run.sh --langgraph # cyclic graph
./setup_and_run.sh --combo # Ralph loop driving the graph
./setup_and_run.sh --help

Or by hand:

python -m venv .venv &&source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt
python examples/ralph_demo.py # → http://localhost:7788

The Ralph demo needs nothing but FastAPI, uvicorn, and websockets. For LangGraph:

pip install langgraph
python examples/langgraph_demo.py # one graph run
python examples/langgraph_demo.py ralph # a Ralph loop driving the graph

A LangGraph run

Installing the package itself is optional — the examples add the repo root to sys.path when run from here. To use it in your own project:

pip install -e .

Hook it into a graph you already have

QUICKSTART-INTEGRATION.md is the hand recipe. In VS Code / Grok, from that project: /loopscope-hook.

importloopscopescope=loopscope.start(open_browser=True)
config=loopscope.attach(app) # returns a LangChain configtry:
app.invoke(state, config=config) # .ainvoke / .stream / .astream all workloopscope.finish(config)
exceptException:
loopscope.finish(config, status="error")
raisescope.hold() # keep the dashboard up after the script ends

attach() publishes the topology and appends a callback handler to whatever config you pass in. Each top-level invocation counts as one pass, so a plain while loop around app.invoke() already fills the tape.

Ralph loops

loop=loopscope.RalphLoop("get the suite green",
phases=["plan", "edit", "test", "review"],
max_iters=20, stall_after=5,
roles={"test": "run the suite"}) # subtitlesforitinloop:
withit.phase("plan") asp:
p.log("picking the two cheapest failures")
withit.phase("edit"):
apply_patch()
withit.phase("test") asp:
failures=run_tests()
p.log(f"{failures} failing")
it.signal(failures, name="failing tests") # the number to drive to zeroit.note(f"{failures} failing")
iffailures==0:
it.done("suite green")

it.done() sets a flag rather than raising — an exception thrown in a for body never reaches the generator that yielded it, so raising would skip the loop's own bookkeeping. The rest of the pass still runs; break right after if you want out immediately.

The loop stops on done(), max_iters, max_seconds, or stall: when signal has not improved for stall_after passes it says so and quits, which is the failure mode a Ralph loop actually has.

Decorator form:

@loopscope.ralph("keep fixing the build", phases=["edit", "test"])defone_pass(it):
...
returnfailures# falsy return converges the loop

Both at once

A Ralph loop outside, a graph inside, one timeline — graph nodes on the left, Ralph passes on the tape:

loop=loopscope.RalphLoop("draft until it scores")
config=loop.attach_graph(app)
foritinloop:
state=app.invoke(state, config=config)
it.signal(1-state["score"], name="distance to 1.0")
ifstate["score"] >=0.9:
it.done("good enough")

Record and replay

loopscope.start(jsonl="runs/tonight.jsonl")
python -m loopscope.replay runs/tonight.jsonl --speed 8

Overnight loops finish while you are asleep; the replay plays back at whatever speed you can stand.

Any loop at all can push to the same dashboard:

loopscope.log("compaction finished", level="warn")
loopscope.metric("open_todos", 14)

What you see

A Ralph loop mid-run

The mesh — your nodes as a constellation. The busiest node (a supervisor, an orchestrator, whatever everything routes through) becomes the hub; the rest orbit it. A plain cycle has no such node, so a Ralph loop puts its objective and pass counter in the middle instead. Each node carries a ring showing its share of the run's time, a status pill, and a subtitle taken from its docstring. When control moves, a dot rides the link it moved along.

Stage stepper — the graph's layers as numbered stages, with the one currently executing lit.

Pipeline / Queue / Node status — left rail. Time share per node, then a compact pass counter (left, running, done, revisits, errors — it does not grow), then each node's hit count. LangGraph runs do not enqueue a long work list here.

Passes — one column per pass, one row per node. Skipped nodes leave gaps, revisits show a count, and the bar under each column is your convergence signal, coloured red at the worst value seen and green at zero. Twenty passes read as a fabric: convergence is a staircase, thrashing is noise. Single-pass runs get a throughput chart here instead. Throughput and System sit as an equal pair under the mesh.

Live feed — timestamped node, tool and model lines, colour-coded per node.

System — node runs, average and slowest, edges exercised, error rate, and why the run stopped.

How it fits together

your code ──► EventBus ──► websocket ──► dashboard
(ring buffer, thread-safe, optional JSONL)
  • The server runs on its own thread with its own event loop, so start() returns immediately and publishing is safe from any thread — sync .invoke() on the main thread, an async app on its own loop, a worker pool.
  • The ring buffer (4000 events; topology is pinned and never evicted) is replayed to every new browser. Opening the tab halfway through a run still draws the graph and recent history. Overnight loops that wrap the buffer lose early log lines — pass jsonl= if you need the full tape.
  • Layout (layering, cycle-breaking, hub detection, orbit placement, colour assignment) is computed in Python; the browser draws coordinates and stretches the orbit to whatever aspect the pane has.
  • raise_error = False on the handler and swallowed exceptions in publish(): a broken dashboard must never take down the run it is watching.

Default bind is 127.0.0.1:7788. Change it with loopscope.start(port=...).

Notes and limits

  • Node subtitles come from the first line of each node function's docstring, or from roles={"node": "text"} if you would rather say it explicitly.
  • Node identity comes from metadata["langgraph_node"], filtered to runs tagged graph:step:N. Without that filter, routers and chains inside a node double-count as node executions.
  • Edge traversal is inferred from execution order, which is exact for sequential graphs and approximate under parallel fan-out.
  • State deltas are truncated for the wire (600 chars, 40 keys, 12 list items). Obvious secret-shaped keys are redacted. The dashboard is for watching, not for auditing.
  • Python 3.10 or newer.
  • Bind to 127.0.0.1 unless you mean it; there is no auth. Binding 0.0.0.0 prints a warning. The websocket accepts same-origin browsers and loopback clients only.

License

Apache-2.0. See LICENSE.

About

Local live dashboard for LangGraph graphs and Ralph loops. Apache 2.0.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

loopscope

Python 3.10+LangGraphStatusLocalLicense

A local live dashboard for LangGraph state graphs and Ralph loops (retry-until-done: plan / edit / test until a number hits zero).

Your agent is already running. loopscope sits in the same process, opens http://127.0.0.1:7788, and shows the run as it happens: which node is hot, how the graph is wired, whether this pass is better than the last, and a colour-coded feed of tools and models. One hook. No collector, no database, no accounts.

It does not rewrite the graph. It does not wrap your nodes. Forget the hook and the agent still runs — the dashboard just stays empty.

0.1 Beta. Loopback only; there is no auth.

loopscope dashboard

Quick start

git clone https://github.com/sw30labs/loopscope.git
cd loopscope
./setup_and_run.sh # venv + tests + Ralph demo → http://localhost:7788
./setup_and_run.sh --langgraph # cyclic graph
./setup_and_run.sh --combo # Ralph loop driving the graph
./setup_and_run.sh --help

Or by hand:

python -m venv .venv &&source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt
python examples/ralph_demo.py # → http://localhost:7788

The Ralph demo needs nothing but FastAPI, uvicorn, and websockets. For LangGraph:

pip install langgraph
python examples/langgraph_demo.py # one graph run
python examples/langgraph_demo.py ralph # a Ralph loop driving the graph

A LangGraph run

Installing the package itself is optional — the examples add the repo root to sys.path when run from here. To use it in your own project:

pip install -e .

Hook it into a graph you already have

QUICKSTART-INTEGRATION.md is the hand recipe. In VS Code / Grok, from that project: /loopscope-hook.

importloopscopescope=loopscope.start(open_browser=True)
config=loopscope.attach(app) # returns a LangChain configtry:
app.invoke(state, config=config) # .ainvoke / .stream / .astream all workloopscope.finish(config)
exceptException:
loopscope.finish(config, status="error")
raisescope.hold() # keep the dashboard up after the script ends

attach() publishes the topology and appends a callback handler to whatever config you pass in. Each top-level invocation counts as one pass, so a plain while loop around app.invoke() already fills the tape.

Ralph loops

loop=loopscope.RalphLoop("get the suite green",
phases=["plan", "edit", "test", "review"],
max_iters=20, stall_after=5,
roles={"test": "run the suite"}) # subtitlesforitinloop:
withit.phase("plan") asp:
p.log("picking the two cheapest failures")
withit.phase("edit"):
apply_patch()
withit.phase("test") asp:
failures=run_tests()
p.log(f"{failures} failing")
it.signal(failures, name="failing tests") # the number to drive to zeroit.note(f"{failures} failing")
iffailures==0:
it.done("suite green")

it.done() sets a flag rather than raising — an exception thrown in a for body never reaches the generator that yielded it, so raising would skip the loop's own bookkeeping. The rest of the pass still runs; break right after if you want out immediately.

The loop stops on done(), max_iters, max_seconds, or stall: when signal has not improved for stall_after passes it says so and quits, which is the failure mode a Ralph loop actually has.

Decorator form:

@loopscope.ralph("keep fixing the build", phases=["edit", "test"])defone_pass(it):
...
returnfailures# falsy return converges the loop

Both at once

A Ralph loop outside, a graph inside, one timeline — graph nodes on the left, Ralph passes on the tape:

loop=loopscope.RalphLoop("draft until it scores")
config=loop.attach_graph(app)
foritinloop:
state=app.invoke(state, config=config)
it.signal(1-state["score"], name="distance to 1.0")
ifstate["score"] >=0.9:
it.done("good enough")

Record and replay

loopscope.start(jsonl="runs/tonight.jsonl")
python -m loopscope.replay runs/tonight.jsonl --speed 8

Overnight loops finish while you are asleep; the replay plays back at whatever speed you can stand.

Any loop at all can push to the same dashboard:

loopscope.log("compaction finished", level="warn")
loopscope.metric("open_todos", 14)

What you see

A Ralph loop mid-run

The mesh — your nodes as a constellation. The busiest node (a supervisor, an orchestrator, whatever everything routes through) becomes the hub; the rest orbit it. A plain cycle has no such node, so a Ralph loop puts its objective and pass counter in the middle instead. Each node carries a ring showing its share of the run's time, a status pill, and a subtitle taken from its docstring. When control moves, a dot rides the link it moved along.

Stage stepper — the graph's layers as numbered stages, with the one currently executing lit.

Pipeline / Queue / Node status — left rail. Time share per node, then a compact pass counter (left, running, done, revisits, errors — it does not grow), then each node's hit count. LangGraph runs do not enqueue a long work list here.

Passes — one column per pass, one row per node. Skipped nodes leave gaps, revisits show a count, and the bar under each column is your convergence signal, coloured red at the worst value seen and green at zero. Twenty passes read as a fabric: convergence is a staircase, thrashing is noise. Single-pass runs get a throughput chart here instead. Throughput and System sit as an equal pair under the mesh.

Live feed — timestamped node, tool and model lines, colour-coded per node.

System — node runs, average and slowest, edges exercised, error rate, and why the run stopped.

How it fits together

your code ──► EventBus ──► websocket ──► dashboard
(ring buffer, thread-safe, optional JSONL)
  • The server runs on its own thread with its own event loop, so start() returns immediately and publishing is safe from any thread — sync .invoke() on the main thread, an async app on its own loop, a worker pool.
  • The ring buffer (4000 events; topology is pinned and never evicted) is replayed to every new browser. Opening the tab halfway through a run still draws the graph and recent history. Overnight loops that wrap the buffer lose early log lines — pass jsonl= if you need the full tape.
  • Layout (layering, cycle-breaking, hub detection, orbit placement, colour assignment) is computed in Python; the browser draws coordinates and stretches the orbit to whatever aspect the pane has.
  • raise_error = False on the handler and swallowed exceptions in publish(): a broken dashboard must never take down the run it is watching.

Default bind is 127.0.0.1:7788. Change it with loopscope.start(port=...).

Notes and limits

  • Node subtitles come from the first line of each node function's docstring, or from roles={"node": "text"} if you would rather say it explicitly.
  • Node identity comes from metadata["langgraph_node"], filtered to runs tagged graph:step:N. Without that filter, routers and chains inside a node double-count as node executions.
  • Edge traversal is inferred from execution order, which is exact for sequential graphs and approximate under parallel fan-out.
  • State deltas are truncated for the wire (600 chars, 40 keys, 12 list items). Obvious secret-shaped keys are redacted. The dashboard is for watching, not for auditing.
  • Python 3.10 or newer.
  • Bind to 127.0.0.1 unless you mean it; there is no auth. Binding 0.0.0.0 prints a warning. The websocket accepts same-origin browsers and loopback clients only.

License

Apache-2.0. See LICENSE.

About

Local live dashboard for LangGraph graphs and Ralph loops. Apache 2.0.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

loopscope

Python 3.10+LangGraphStatusLocalLicense

A local live dashboard for LangGraph state graphs and Ralph loops (retry-until-done: plan / edit / test until a number hits zero).

Your agent is already running. loopscope sits in the same process, opens http://127.0.0.1:7788, and shows the run as it happens: which node is hot, how the graph is wired, whether this pass is better than the last, and a colour-coded feed of tools and models. One hook. No collector, no database, no accounts.

It does not rewrite the graph. It does not wrap your nodes. Forget the hook and the agent still runs — the dashboard just stays empty.

0.1 Beta. Loopback only; there is no auth.

loopscope dashboard

Quick start

git clone https://github.com/sw30labs/loopscope.git
cd loopscope
./setup_and_run.sh # venv + tests + Ralph demo → http://localhost:7788
./setup_and_run.sh --langgraph # cyclic graph
./setup_and_run.sh --combo # Ralph loop driving the graph
./setup_and_run.sh --help

Or by hand:

python -m venv .venv &&source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt
python examples/ralph_demo.py # → http://localhost:7788

The Ralph demo needs nothing but FastAPI, uvicorn, and websockets. For LangGraph:

pip install langgraph
python examples/langgraph_demo.py # one graph run
python examples/langgraph_demo.py ralph # a Ralph loop driving the graph

A LangGraph run

Installing the package itself is optional — the examples add the repo root to sys.path when run from here. To use it in your own project:

pip install -e .

Hook it into a graph you already have

QUICKSTART-INTEGRATION.md is the hand recipe. In VS Code / Grok, from that project: /loopscope-hook.

importloopscopescope=loopscope.start(open_browser=True)
config=loopscope.attach(app) # returns a LangChain configtry:
app.invoke(state, config=config) # .ainvoke / .stream / .astream all workloopscope.finish(config)
exceptException:
loopscope.finish(config, status="error")
raisescope.hold() # keep the dashboard up after the script ends

attach() publishes the topology and appends a callback handler to whatever config you pass in. Each top-level invocation counts as one pass, so a plain while loop around app.invoke() already fills the tape.

Ralph loops

loop=loopscope.RalphLoop("get the suite green",
phases=["plan", "edit", "test", "review"],
max_iters=20, stall_after=5,
roles={"test": "run the suite"}) # subtitlesforitinloop:
withit.phase("plan") asp:
p.log("picking the two cheapest failures")
withit.phase("edit"):
apply_patch()
withit.phase("test") asp:
failures=run_tests()
p.log(f"{failures} failing")
it.signal(failures, name="failing tests") # the number to drive to zeroit.note(f"{failures} failing")
iffailures==0:
it.done("suite green")

it.done() sets a flag rather than raising — an exception thrown in a for body never reaches the generator that yielded it, so raising would skip the loop's own bookkeeping. The rest of the pass still runs; break right after if you want out immediately.

The loop stops on done(), max_iters, max_seconds, or stall: when signal has not improved for stall_after passes it says so and quits, which is the failure mode a Ralph loop actually has.

Decorator form:

@loopscope.ralph("keep fixing the build", phases=["edit", "test"])defone_pass(it):
...
returnfailures# falsy return converges the loop

Both at once

A Ralph loop outside, a graph inside, one timeline — graph nodes on the left, Ralph passes on the tape:

loop=loopscope.RalphLoop("draft until it scores")
config=loop.attach_graph(app)
foritinloop:
state=app.invoke(state, config=config)
it.signal(1-state["score"], name="distance to 1.0")
ifstate["score"] >=0.9:
it.done("good enough")

Record and replay

loopscope.start(jsonl="runs/tonight.jsonl")
python -m loopscope.replay runs/tonight.jsonl --speed 8

Overnight loops finish while you are asleep; the replay plays back at whatever speed you can stand.

Any loop at all can push to the same dashboard:

loopscope.log("compaction finished", level="warn")
loopscope.metric("open_todos", 14)

What you see

A Ralph loop mid-run

The mesh — your nodes as a constellation. The busiest node (a supervisor, an orchestrator, whatever everything routes through) becomes the hub; the rest orbit it. A plain cycle has no such node, so a Ralph loop puts its objective and pass counter in the middle instead. Each node carries a ring showing its share of the run's time, a status pill, and a subtitle taken from its docstring. When control moves, a dot rides the link it moved along.

Stage stepper — the graph's layers as numbered stages, with the one currently executing lit.

Pipeline / Queue / Node status — left rail. Time share per node, then a compact pass counter (left, running, done, revisits, errors — it does not grow), then each node's hit count. LangGraph runs do not enqueue a long work list here.

Passes — one column per pass, one row per node. Skipped nodes leave gaps, revisits show a count, and the bar under each column is your convergence signal, coloured red at the worst value seen and green at zero. Twenty passes read as a fabric: convergence is a staircase, thrashing is noise. Single-pass runs get a throughput chart here instead. Throughput and System sit as an equal pair under the mesh.

Live feed — timestamped node, tool and model lines, colour-coded per node.

System — node runs, average and slowest, edges exercised, error rate, and why the run stopped.

How it fits together

your code ──► EventBus ──► websocket ──► dashboard
(ring buffer, thread-safe, optional JSONL)
  • The server runs on its own thread with its own event loop, so start() returns immediately and publishing is safe from any thread — sync .invoke() on the main thread, an async app on its own loop, a worker pool.
  • The ring buffer (4000 events; topology is pinned and never evicted) is replayed to every new browser. Opening the tab halfway through a run still draws the graph and recent history. Overnight loops that wrap the buffer lose early log lines — pass jsonl= if you need the full tape.
  • Layout (layering, cycle-breaking, hub detection, orbit placement, colour assignment) is computed in Python; the browser draws coordinates and stretches the orbit to whatever aspect the pane has.
  • raise_error = False on the handler and swallowed exceptions in publish(): a broken dashboard must never take down the run it is watching.

Default bind is 127.0.0.1:7788. Change it with loopscope.start(port=...).

Notes and limits

  • Node subtitles come from the first line of each node function's docstring, or from roles={"node": "text"} if you would rather say it explicitly.
  • Node identity comes from metadata["langgraph_node"], filtered to runs tagged graph:step:N. Without that filter, routers and chains inside a node double-count as node executions.
  • Edge traversal is inferred from execution order, which is exact for sequential graphs and approximate under parallel fan-out.
  • State deltas are truncated for the wire (600 chars, 40 keys, 12 list items). Obvious secret-shaped keys are redacted. The dashboard is for watching, not for auditing.
  • Python 3.10 or newer.
  • Bind to 127.0.0.1 unless you mean it; there is no auth. Binding 0.0.0.0 prints a warning. The websocket accepts same-origin browsers and loopback clients only.

License

Apache-2.0. See LICENSE.

About

Local live dashboard for LangGraph graphs and Ralph loops. Apache 2.0.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

loopscope

Python 3.10+LangGraphStatusLocalLicense

A local live dashboard for LangGraph state graphs and Ralph loops (retry-until-done: plan / edit / test until a number hits zero).

Your agent is already running. loopscope sits in the same process, opens http://127.0.0.1:7788, and shows the run as it happens: which node is hot, how the graph is wired, whether this pass is better than the last, and a colour-coded feed of tools and models. One hook. No collector, no database, no accounts.

It does not rewrite the graph. It does not wrap your nodes. Forget the hook and the agent still runs — the dashboard just stays empty.

0.1 Beta. Loopback only; there is no auth.

loopscope dashboard

Quick start

git clone https://github.com/sw30labs/loopscope.git
cd loopscope
./setup_and_run.sh # venv + tests + Ralph demo → http://localhost:7788
./setup_and_run.sh --langgraph # cyclic graph
./setup_and_run.sh --combo # Ralph loop driving the graph
./setup_and_run.sh --help

Or by hand:

python -m venv .venv &&source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt
python examples/ralph_demo.py # → http://localhost:7788

The Ralph demo needs nothing but FastAPI, uvicorn, and websockets. For LangGraph:

pip install langgraph
python examples/langgraph_demo.py # one graph run
python examples/langgraph_demo.py ralph # a Ralph loop driving the graph

A LangGraph run

Installing the package itself is optional — the examples add the repo root to sys.path when run from here. To use it in your own project:

pip install -e .

Hook it into a graph you already have

QUICKSTART-INTEGRATION.md is the hand recipe. In VS Code / Grok, from that project: /loopscope-hook.

importloopscopescope=loopscope.start(open_browser=True)
config=loopscope.attach(app) # returns a LangChain configtry:
app.invoke(state, config=config) # .ainvoke / .stream / .astream all workloopscope.finish(config)
exceptException:
loopscope.finish(config, status="error")
raisescope.hold() # keep the dashboard up after the script ends

attach() publishes the topology and appends a callback handler to whatever config you pass in. Each top-level invocation counts as one pass, so a plain while loop around app.invoke() already fills the tape.

Ralph loops

loop=loopscope.RalphLoop("get the suite green",
phases=["plan", "edit", "test", "review"],
max_iters=20, stall_after=5,
roles={"test": "run the suite"}) # subtitlesforitinloop:
withit.phase("plan") asp:
p.log("picking the two cheapest failures")
withit.phase("edit"):
apply_patch()
withit.phase("test") asp:
failures=run_tests()
p.log(f"{failures} failing")
it.signal(failures, name="failing tests") # the number to drive to zeroit.note(f"{failures} failing")
iffailures==0:
it.done("suite green")

it.done() sets a flag rather than raising — an exception thrown in a for body never reaches the generator that yielded it, so raising would skip the loop's own bookkeeping. The rest of the pass still runs; break right after if you want out immediately.

The loop stops on done(), max_iters, max_seconds, or stall: when signal has not improved for stall_after passes it says so and quits, which is the failure mode a Ralph loop actually has.

Decorator form:

@loopscope.ralph("keep fixing the build", phases=["edit", "test"])defone_pass(it):
...
returnfailures# falsy return converges the loop

Both at once

A Ralph loop outside, a graph inside, one timeline — graph nodes on the left, Ralph passes on the tape:

loop=loopscope.RalphLoop("draft until it scores")
config=loop.attach_graph(app)
foritinloop:
state=app.invoke(state, config=config)
it.signal(1-state["score"], name="distance to 1.0")
ifstate["score"] >=0.9:
it.done("good enough")

Record and replay

loopscope.start(jsonl="runs/tonight.jsonl")
python -m loopscope.replay runs/tonight.jsonl --speed 8

Overnight loops finish while you are asleep; the replay plays back at whatever speed you can stand.

Any loop at all can push to the same dashboard:

loopscope.log("compaction finished", level="warn")
loopscope.metric("open_todos", 14)

What you see

A Ralph loop mid-run

The mesh — your nodes as a constellation. The busiest node (a supervisor, an orchestrator, whatever everything routes through) becomes the hub; the rest orbit it. A plain cycle has no such node, so a Ralph loop puts its objective and pass counter in the middle instead. Each node carries a ring showing its share of the run's time, a status pill, and a subtitle taken from its docstring. When control moves, a dot rides the link it moved along.

Stage stepper — the graph's layers as numbered stages, with the one currently executing lit.

Pipeline / Queue / Node status — left rail. Time share per node, then a compact pass counter (left, running, done, revisits, errors — it does not grow), then each node's hit count. LangGraph runs do not enqueue a long work list here.

Passes — one column per pass, one row per node. Skipped nodes leave gaps, revisits show a count, and the bar under each column is your convergence signal, coloured red at the worst value seen and green at zero. Twenty passes read as a fabric: convergence is a staircase, thrashing is noise. Single-pass runs get a throughput chart here instead. Throughput and System sit as an equal pair under the mesh.

Live feed — timestamped node, tool and model lines, colour-coded per node.

System — node runs, average and slowest, edges exercised, error rate, and why the run stopped.

How it fits together

your code ──► EventBus ──► websocket ──► dashboard
(ring buffer, thread-safe, optional JSONL)
  • The server runs on its own thread with its own event loop, so start() returns immediately and publishing is safe from any thread — sync .invoke() on the main thread, an async app on its own loop, a worker pool.
  • The ring buffer (4000 events; topology is pinned and never evicted) is replayed to every new browser. Opening the tab halfway through a run still draws the graph and recent history. Overnight loops that wrap the buffer lose early log lines — pass jsonl= if you need the full tape.
  • Layout (layering, cycle-breaking, hub detection, orbit placement, colour assignment) is computed in Python; the browser draws coordinates and stretches the orbit to whatever aspect the pane has.
  • raise_error = False on the handler and swallowed exceptions in publish(): a broken dashboard must never take down the run it is watching.

Default bind is 127.0.0.1:7788. Change it with loopscope.start(port=...).

Notes and limits

  • Node subtitles come from the first line of each node function's docstring, or from roles={"node": "text"} if you would rather say it explicitly.
  • Node identity comes from metadata["langgraph_node"], filtered to runs tagged graph:step:N. Without that filter, routers and chains inside a node double-count as node executions.
  • Edge traversal is inferred from execution order, which is exact for sequential graphs and approximate under parallel fan-out.
  • State deltas are truncated for the wire (600 chars, 40 keys, 12 list items). Obvious secret-shaped keys are redacted. The dashboard is for watching, not for auditing.
  • Python 3.10 or newer.
  • Bind to 127.0.0.1 unless you mean it; there is no auth. Binding 0.0.0.0 prints a warning. The websocket accepts same-origin browsers and loopback clients only.

License

Apache-2.0. See LICENSE.

About

Local live dashboard for LangGraph graphs and Ralph loops. Apache 2.0.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages