Skip to content

Commit bee8dd4

Browse files
rsamfclaude
andcommitted
feat: named text streams, canonical nebo:// refs, and UI/perf work
Implements the NEW.md batch. The through-line is that text becomes a first-class named stream like metrics and media, and every resource gets an unambiguous, clickable identifier. Text redesign (breaking): - nb.log(message, name=) -> nb.log_text(name, message, step=), with name a required first positional, matching the metric helpers. There is no level/severity anywhere; the old info/warn/error UI was dead code for every SDK-produced run. nb.log() survives only as a deprecated shim that forwards and warns once per process. - Wire/file type is now "text" (entry code 9). The legacy "log" spelling (code 0) stays readable forever: ingest normalizes it, so existing .nebo files decode unchanged. - Renamed through every surface: /runs/{id}/text, `nebo text ls`, nebo_get_text, cache table `texts` (schema v6), ?text=NAME embeds. - Text renders like images: one card per stream name, each entry a header row (name, step, timestamp, actions) over the payload. Canonical nebo:// references: - nebo://run/<id>[/<loggable>[/<name...>]][@<step>] and nebo://group/<path>, parsed by twin implementations in nebo/core/refs.py and ui/src/lib/refs.ts. The loggable is always one segment, so stream names containing '/' need no escaping. - Deep links navigate deterministically: force the flat view, open the tab/filter owning the card, scroll to it, flash it for 3s. Shared modality order keeps desktop and mobile in lockstep. - New GET /resolve?ref= returns parsed components plus existence. - @nb.fn now guarantees unique loggable ids: two different functions claiming the same qualname no longer merge into one node. Performance (a 112 MB / 858k-point run opened slowly): - GET /runs/{id}/metrics decimates per series (min/max buckets, ~2k points, ?points=0 for full) and reports total_points/downsampled. The UI paints the decimated response, then fetches full fidelity so every datapoint still reaches the user. - Watcher deepening no longer broadcasts a file's whole history over WebSocket, ingest yields between slices, and the store pools metric appends per series per batch. - Measured: /metrics 69.7 MB -> 5.2 MB, warm read 6.9s -> 0.77s. UI: - DAG auto-layout fixes on desktop and the mobile canvas (links to a run could render unlaid-out), horizontal desktop default, solid edges, no pane context menu. - Right panel hosts Markdown / Config / Settings as icon tabs, open by default and resizable; config renders as a structured tree. - Top bar reduced to <group>/<name> <id> <date>. - Groups only expand in the tree; their docs are tree leaves opening a dedicated viewer. No client-side run rename; tooltips drop counts that read 0 for shallow runs. - Tracker: step navigation works while collapsed, modality chips moved under the stream search, collapsed branches merge their datapoints. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XrJfA61nnwkwJ86UiSvYYN
1 parent 5d258ed commit bee8dd4

124 files changed

Lines changed: 3212 additions & 1953 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CLAUDE.md

Lines changed: 92 additions & 22 deletions
Large diffs are not rendered by default.

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ Agent skills are released in the package preparing coding agents to not only wri
4848

4949
```mermaid
5050
graph LR
51-
A[Your Python Pipeline] --> B[Nebo SDK<br>@fn, log, track, ...]
51+
A[Your Python Pipeline] --> B[Nebo SDK<br>@fn, log_text, track, ...]
5252
B --> C[Daemon Server<br>FastAPI, port 7861]
5353
B --> D[Terminal Dashboard<br>Rich]
5454
C --> E[CLI<br>nebo]

docs/cli.rst

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ subcommands:
88

99
* **Server & admin** — start/stop the daemon, manage its cache, install
1010
agent skills, deploy to a Hugging Face Space.
11-
* **Agent-callable Q&A and writes** — read runs, logs, metrics, and the
11+
* **Agent-callable Q&A and writes** — read runs, text, metrics, and the
1212
DAG from a running daemon, manage alert rules, and push new entries
1313
into a run. These are the same commands the shipped agent skills use.
1414

@@ -401,9 +401,11 @@ env var; reorganize afterward from here. Each group can hold markdown docs
401401
{run_id: group}}`` (runs absent from ``runs`` are at the root).
402402
``groups rm`` refuses if the group still has member runs or subgroups —
403403
move them out first (nebo has no run deletion). Group docs support
404-
``nebo://run/<id>``, ``nebo://run/<id>?step=<n>``, and
405-
``nebo://group/<path>`` deep links that the web UI turns into clickable
406-
navigation.
404+
canonical ``nebo://`` references — ``nebo://run/<id>``,
405+
``nebo://run/<id>/<loggable>/<stream>``, any run form suffixed with
406+
``@<step>``, and ``nebo://group/<path>`` — that the web UI turns into
407+
clickable navigation (``?step=<n>`` is still accepted as a legacy
408+
alias).
407409

408410
nebo graph
409411
==========
@@ -421,7 +423,8 @@ nebo loggables
421423

422424
.. option:: show <loggable_id> [--run <run_id>]
423425

424-
Show a loggable's status: metadata, recent logs, metrics, progress.
426+
Show a loggable's status: metadata, recent text entries, metrics,
427+
progress.
425428
Loggables are ``@nb.fn`` node ids (function qualnames), plus the
426429
implicit ``__global__`` and ``__agent__`` loggables.
427430

@@ -437,22 +440,24 @@ docstrings.
437440

438441
Run id (latest if omitted).
439442

440-
nebo logs
441-
=========
443+
nebo text (read)
444+
================
442445

443-
.. program:: nebo logs
446+
.. program:: nebo text ls
444447

445-
.. option:: --run <run_id>
446-
447-
Run ID (latest if omitted).
448+
.. code-block:: bash
448449
449-
.. option:: --node <loggable_id>
450+
nebo text ls
451+
nebo text ls --run run_1748_0 --node train --limit 50
450452
451-
Filter to one loggable.
453+
.. option:: ls [--run <run_id>] [--node <loggable_id>] [--limit <n>]
452454

453-
.. option:: --limit <n>
455+
List text entries, newest last. Human output prints one line per
456+
entry as ``[loggable_id] name@step: message``.
454457

455-
Maximum entries to return (default: ``100``).
458+
* ``--run`` — run ID (latest if omitted).
459+
* ``--node`` — filter to one loggable.
460+
* ``--limit`` — maximum entries to return (default: ``100``).
456461

457462
nebo metrics (read)
458463
===================
@@ -569,15 +574,15 @@ nebo text / images / audio log
569574

570575
.. code-block:: bash
571576
572-
nebo text log --entries-json '[{"message": "analysis complete"}]'
577+
nebo text log --entries-json '[{"name": "analysis", "message": "analysis complete"}]'
573578
nebo images log --entries-json '[{"name": "annotated", "path": "./out.png"}]'
574579
nebo audio log --entries-json '[{"name": "sample", "path": "./clip.wav", "sr": 22050}]'
575580
576581
.. option:: --entries-json <json>
577582

578583
JSON list of entries.
579584

580-
* **text**: ``{loggable_id?, message, name?, level?, step?}``
585+
* **text**: ``{loggable_id?, name?, message, step?}``
581586
* **images**: ``{loggable_id?, name, path? | url? | data?, step?,
582587
labels?}`` — exactly one of ``path`` (local file, read and
583588
encoded by the CLI), ``url`` (fetched server-side), or ``data``

docs/demos/guide/11_depends_on.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@
44
@nb.fn()
55
def setup():
66
"""Initialize shared resources."""
7-
nb.log("Setting up")
7+
nb.log_text("status", "Setting up")
88

99

1010
@nb.fn(depends_on=[setup])
1111
def process():
1212
"""Uses resources initialized by setup."""
13-
nb.log("Processing")
13+
nb.log_text("status", "Processing")
1414

1515

1616
setup()

docs/demos/guide/12_class_grouping.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@
44
@nb.fn()
55
class DataPipeline:
66
def load(self):
7-
nb.log("Loading data")
7+
nb.log_text("status", "Loading data")
88
return [1, 2, 3]
99

1010
def transform(self, data):
11-
nb.log(f"Transforming {len(data)} items")
11+
nb.log_text("status", f"Transforming {len(data)} items")
1212
return [x * 2 for x in data]
1313

1414
def save(self, data):
15-
nb.log(f"Saving {len(data)} items")
15+
nb.log_text("status", f"Saving {len(data)} items")
1616

1717

1818
p = DataPipeline()

docs/demos/guide/13_workflow_md.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
@nb.fn()
1212
def classify():
13-
nb.log("Classifying images")
13+
nb.log_text("status", "Classifying images")
1414
nb.log_line("accuracy", 0.92)
1515

1616

docs/demos/guide/14_ui_hints.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@
55

66
@nb.fn(ui={"color": "#fb923c"})
77
def data_loader():
8-
nb.log("Loading data")
8+
nb.log_text("status", "Loading data")
99
return [1, 2, 3]
1010

1111

1212
@nb.fn(ui={"color": "#34d399", "default_tab": "metrics"})
1313
def train(data):
14-
nb.log(f"Training on {len(data)} items")
14+
nb.log_text("status", f"Training on {len(data)} items")
1515
for step in range(20):
1616
nb.log_line("loss", 1.0 / (step + 1))
1717

docs/demos/guide/15_complete_pipeline.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ def generate(num_samples: int = 200, noise: float = 0.1, seed: int = 42):
1111
rng = np.random.default_rng(seed)
1212
t = np.linspace(0, 4 * np.pi, num_samples)
1313
signal = np.sin(t) + noise * rng.standard_normal(num_samples)
14-
nb.log(f"Generated {num_samples} samples")
14+
nb.log_text("status", f"Generated {num_samples} samples")
1515
return signal
1616

1717

@@ -22,14 +22,14 @@ def normalize(data, method: str = "standard", clip_min: float = -3.0, clip_max:
2222
if method == "standard":
2323
data = (data - data.mean()) / (data.std() + 1e-8)
2424
data = np.clip(data, clip_min, clip_max)
25-
nb.log(f"Normalized with method={method}")
25+
nb.log_text("status", f"Normalized with method={method}")
2626
return data
2727

2828

2929
@nb.fn()
3030
def analyze(data):
3131
"""Compute statistics on the processed data."""
32-
nb.log(f"Stats: mean={data.mean():.4f}, std={data.std():.4f}")
32+
nb.log_text("stats", f"Stats: mean={data.mean():.4f}, std={data.std():.4f}")
3333
nb.log_line("mean", float(data.mean()))
3434
nb.log_line("std", float(data.std()))
3535
return {"mean": float(data.mean()), "std": float(data.std()), "n": len(data)}

docs/demos/guide/1_text_logs.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import numpy as np
22
import nebo as nb
33

4-
nb.log("Starting training...")
4+
nb.log_text("status", "Starting training...")
55
for epoch in range(3):
66
loss = 1.0 / (epoch + 1)
7-
nb.log(f"Epoch {epoch}: loss={loss:.4f}")
7+
nb.log_text("epochs", f"Epoch {epoch}: loss={loss:.4f}")
88

9-
nb.log(np.random.default_rng(0).standard_normal((32, 64)))
9+
nb.log_text("weights", np.random.default_rng(0).standard_normal((32, 64)))

docs/demos/guide/8_global_scope.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import nebo as nb
22

3-
nb.log("environment looks good")
3+
nb.log_text("status", "environment looks good")
44
nb.log_line("warmup_heartbeat", 1.0)
55
nb.log_line("warmup_heartbeat", 1.0)
66
nb.log_line("warmup_heartbeat", 1.0)

0 commit comments

Comments
 (0)