nudemos 0.1.0__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 (47) hide show
  1. nudemos-0.1.0/.gitignore +103 -0
  2. nudemos-0.1.0/PKG-INFO +49 -0
  3. nudemos-0.1.0/README.md +31 -0
  4. nudemos-0.1.0/pyproject.toml +38 -0
  5. nudemos-0.1.0/src/nudemos/__init__.py +43 -0
  6. nudemos-0.1.0/src/nudemos/agents/__init__.py +44 -0
  7. nudemos-0.1.0/src/nudemos/agents/board.py +189 -0
  8. nudemos-0.1.0/src/nudemos/agents/claude.py +31 -0
  9. nudemos-0.1.0/src/nudemos/agents/flows.py +454 -0
  10. nudemos-0.1.0/src/nudemos/agents/job.py +186 -0
  11. nudemos-0.1.0/src/nudemos/agents/launch.py +92 -0
  12. nudemos-0.1.0/src/nudemos/agents/orchestrate.py +260 -0
  13. nudemos-0.1.0/src/nudemos/agents/page.py +208 -0
  14. nudemos-0.1.0/src/nudemos/agents/state.py +151 -0
  15. nudemos-0.1.0/src/nudemos/agents/views.py +436 -0
  16. nudemos-0.1.0/src/nudemos/counter.py +84 -0
  17. nudemos-0.1.0/src/nudemos/memes/__init__.py +42 -0
  18. nudemos-0.1.0/src/nudemos/memes/book.py +959 -0
  19. nudemos-0.1.0/src/nudemos/memes/cells.py +272 -0
  20. nudemos-0.1.0/src/nudemos/memes/chain.py +302 -0
  21. nudemos-0.1.0/src/nudemos/memes/control.py +99 -0
  22. nudemos-0.1.0/src/nudemos/memes/curve.py +81 -0
  23. nudemos-0.1.0/src/nudemos/memes/job.py +319 -0
  24. nudemos-0.1.0/src/nudemos/memes/monitor.py +171 -0
  25. nudemos-0.1.0/src/nudemos/memes/overview.py +153 -0
  26. nudemos-0.1.0/src/nudemos/memes/state.py +128 -0
  27. nudemos-0.1.0/src/nudemos/memes/sync.py +161 -0
  28. nudemos-0.1.0/src/nudemos/memes/token.py +208 -0
  29. nudemos-0.1.0/src/nudemos/memes/views.py +607 -0
  30. nudemos-0.1.0/src/nudemos/mnist/__init__.py +87 -0
  31. nudemos-0.1.0/src/nudemos/mnist/compare.py +136 -0
  32. nudemos-0.1.0/src/nudemos/mnist/ink.py +267 -0
  33. nudemos-0.1.0/src/nudemos/mnist/interactions.py +193 -0
  34. nudemos-0.1.0/src/nudemos/mnist/job.py +198 -0
  35. nudemos-0.1.0/src/nudemos/mnist/launch.py +167 -0
  36. nudemos-0.1.0/src/nudemos/mnist/read.py +245 -0
  37. nudemos-0.1.0/src/nudemos/mnist/reader.py +149 -0
  38. nudemos-0.1.0/src/nudemos/mnist/run.py +287 -0
  39. nudemos-0.1.0/src/nudemos/mnist/state.py +141 -0
  40. nudemos-0.1.0/src/nudemos/mnist/trainer.py +406 -0
  41. nudemos-0.1.0/src/nudemos/mnist/views.py +476 -0
  42. nudemos-0.1.0/src/nudemos/movies/__init__.py +46 -0
  43. nudemos-0.1.0/src/nudemos/movies/cells.py +681 -0
  44. nudemos-0.1.0/src/nudemos/movies/data.py +254 -0
  45. nudemos-0.1.0/src/nudemos/movies/kit.py +500 -0
  46. nudemos-0.1.0/src/nudemos/sampled.py +136 -0
  47. nudemos-0.1.0/src/nudemos/tour.py +46 -0
@@ -0,0 +1,103 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ # The nuspace-ui package links its build/ to the vite output so an editable
13
+ # install serves it. A symlink, not a directory, but say so explicitly.
14
+ !pkgs/ts/nuspace-ui/python/nuspace_ui/build
15
+ develop-eggs/
16
+ dist/
17
+ downloads/
18
+ eggs/
19
+ .eggs/
20
+ lib64/
21
+ parts/
22
+ sdist/
23
+ var/
24
+ wheels/
25
+ share/python-wheels/
26
+ *.egg-info/
27
+ .installed.cfg
28
+ *.egg
29
+ MANIFEST
30
+
31
+ # Installer logs
32
+ pip-log.txt
33
+ pip-delete-this-directory.txt
34
+
35
+ # Unit test / coverage reports
36
+ htmlcov/
37
+ .tox/
38
+ .nox/
39
+ .coverage
40
+ .coverage.*
41
+ .cache
42
+ nosetests.xml
43
+ coverage.xml
44
+ *.cover
45
+ *.py,cover
46
+ .hypothesis/
47
+ .pytest_cache/
48
+ cover/
49
+
50
+ # Environments
51
+ .env
52
+ .venv
53
+ env/
54
+ venv/
55
+ ENV/
56
+ env.bak/
57
+ venv.bak/
58
+
59
+ # uv
60
+ uv.lock
61
+
62
+ # mypy
63
+ .mypy_cache/
64
+ .dmypy.json
65
+ dmypy.json
66
+
67
+ # IDEs
68
+ .idea/
69
+ .vscode/
70
+
71
+ .DS_Store
72
+
73
+ # Profiling
74
+ *.prof
75
+ *.lprof
76
+
77
+ # Benchmarking
78
+ benchmark_results/
79
+ *.benchmark
80
+ *.benchmarks
81
+
82
+ # Ruff
83
+ .ruff_cache/
84
+
85
+ # Misc
86
+ /*playground
87
+ .db*/
88
+ .state*/
89
+ .logs*/
90
+
91
+ # syncthing
92
+ .stfolder
93
+
94
+ # nuspace-ui (vite / node)
95
+ node_modules/
96
+ !src/nuspace/web/ui/dist/
97
+ src/nuspace/web/ui/dist/*
98
+ !src/nuspace/web/ui/dist/.gitkeep
99
+
100
+ *.db/
101
+ *.nuspace/
102
+ # Local archive of the v0 prototype (reference only, never edited)
103
+ _archive/
nudemos-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,49 @@
1
+ Metadata-Version: 2.5
2
+ Name: nudemos
3
+ Version: 0.1.0
4
+ Summary: The Nu demos as nuspace Planes: a counter, a sampled series, a film base and MNIST trained live.
5
+ Project-URL: Repository, https://github.com/nustackdev/nuverse
6
+ Author-email: Gor Arakelyan <gorarkln@gmail.com>
7
+ License-Expression: Apache-2.0
8
+ Requires-Python: >=3.10
9
+ Requires-Dist: nuspace
10
+ Provides-Extra: agents
11
+ Requires-Dist: nustd[cc]; extra == 'agents'
12
+ Provides-Extra: memes
13
+ Requires-Dist: nustd[http]; extra == 'memes'
14
+ Provides-Extra: mnist
15
+ Requires-Dist: torch>=2.2; extra == 'mnist'
16
+ Requires-Dist: torchvision>=0.17; extra == 'mnist'
17
+ Description-Content-Type: text/markdown
18
+
19
+ # nudemos
20
+
21
+ The Nu demos as [nuspace](https://github.com/nustackdev/nuspace) Planes `+` creates, under Demos:
22
+
23
+ - **Counter**: a count ticking once a second, kept in its cell's state.
24
+ - **Sampled**: a series that grows forever, charted from a reservoir sample.
25
+ - **Movies**: a film base where every film and person is its own live page.
26
+ - **MNIST**: train a small CNN, steer it while it trains, and track every run. Needs the `mnist` extra.
27
+ - **Memes**: a live terminal over the meme market on Solana: launches, trades and every coin's page, synced from public mainnet. Needs the `memes` extra.
28
+ - **Agents**: Claude Code sessions run, watched, steered and orchestrated from a space. One agent is one Claude Code session in a page, driven by a short Nu loop; fan-out with a judge, chains and task boards are a few lines of Nu each. Needs the `agents` extra and a logged-in `claude` CLI, no API key.
29
+ - **Nu demos**: all three at once, nested under one plane.
30
+
31
+ ```
32
+ pip install nudemos
33
+ ```
34
+
35
+ nuspace finds it on install through the `nuspace.extensions` entry point. You don't import it yourself.
36
+
37
+ ## MNIST
38
+
39
+ Launch picks the knobs (optimizer, learning rate, batch size, channels, epochs, dropout) and starts a run: a page, and a headless job under it that trains in its own process. A run page shows the curves, twelve test digits and what the model reads in each, and every layer's weights and gradients over training. Drag the learning rate while it trains, stop it, resume it. Compare lines the runs up. Read puts a run's model to work: pick a run and its latest or best checkpoint, draw a digit, and it reads it on every stroke, showing the 28x28 image the model sees. Pick a run that is still training, or just launch one (the page follows the newest run until you pick), and the same drawing is read again as each checkpoint lands.
40
+
41
+ Nu owns the loop, torch owns the step: the job cell is a Nu loop over a trainer resource, reading its learning rate from state each chunk. Close the space mid run and open it again: the run carries on from its last checkpoint.
42
+
43
+ Models live in the space, not in files: each run keeps its latest checkpoint (what it resumes from) and the weights at its best test score, in its own state.
44
+
45
+ ```
46
+ pip install "nudemos[mnist]"
47
+ ```
48
+
49
+ MNIST downloads once, on the first run, into `~/.cache/nuverse-mnist` (`$NUVERSE_MNIST_DATA` to move it). Runs train on cuda, or mps on Apple silicon, else the cpu (`$NUVERSE_MNIST_DEVICE` to pick). Three epochs take about a minute on a laptop cpu.
@@ -0,0 +1,31 @@
1
+ # nudemos
2
+
3
+ The Nu demos as [nuspace](https://github.com/nustackdev/nuspace) Planes `+` creates, under Demos:
4
+
5
+ - **Counter**: a count ticking once a second, kept in its cell's state.
6
+ - **Sampled**: a series that grows forever, charted from a reservoir sample.
7
+ - **Movies**: a film base where every film and person is its own live page.
8
+ - **MNIST**: train a small CNN, steer it while it trains, and track every run. Needs the `mnist` extra.
9
+ - **Memes**: a live terminal over the meme market on Solana: launches, trades and every coin's page, synced from public mainnet. Needs the `memes` extra.
10
+ - **Agents**: Claude Code sessions run, watched, steered and orchestrated from a space. One agent is one Claude Code session in a page, driven by a short Nu loop; fan-out with a judge, chains and task boards are a few lines of Nu each. Needs the `agents` extra and a logged-in `claude` CLI, no API key.
11
+ - **Nu demos**: all three at once, nested under one plane.
12
+
13
+ ```
14
+ pip install nudemos
15
+ ```
16
+
17
+ nuspace finds it on install through the `nuspace.extensions` entry point. You don't import it yourself.
18
+
19
+ ## MNIST
20
+
21
+ Launch picks the knobs (optimizer, learning rate, batch size, channels, epochs, dropout) and starts a run: a page, and a headless job under it that trains in its own process. A run page shows the curves, twelve test digits and what the model reads in each, and every layer's weights and gradients over training. Drag the learning rate while it trains, stop it, resume it. Compare lines the runs up. Read puts a run's model to work: pick a run and its latest or best checkpoint, draw a digit, and it reads it on every stroke, showing the 28x28 image the model sees. Pick a run that is still training, or just launch one (the page follows the newest run until you pick), and the same drawing is read again as each checkpoint lands.
22
+
23
+ Nu owns the loop, torch owns the step: the job cell is a Nu loop over a trainer resource, reading its learning rate from state each chunk. Close the space mid run and open it again: the run carries on from its last checkpoint.
24
+
25
+ Models live in the space, not in files: each run keeps its latest checkpoint (what it resumes from) and the weights at its best test score, in its own state.
26
+
27
+ ```
28
+ pip install "nudemos[mnist]"
29
+ ```
30
+
31
+ MNIST downloads once, on the first run, into `~/.cache/nuverse-mnist` (`$NUVERSE_MNIST_DATA` to move it). Runs train on cuda, or mps on Apple silicon, else the cpu (`$NUVERSE_MNIST_DEVICE` to pick). Three epochs take about a minute on a laptop cpu.
@@ -0,0 +1,38 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "nudemos"
7
+ version = "0.1.0"
8
+ description = "The Nu demos as nuspace Planes: a counter, a sampled series, a film base and MNIST trained live."
9
+ readme = "README.md"
10
+ license = "Apache-2.0"
11
+ authors = [{ name = "Gor Arakelyan", email = "gorarkln@gmail.com" }]
12
+ requires-python = ">=3.10"
13
+ dependencies = [
14
+ "nuspace",
15
+ ]
16
+
17
+ [project.optional-dependencies]
18
+ mnist = [
19
+ "torch>=2.2",
20
+ "torchvision>=0.17",
21
+ ]
22
+ # The memes demo's JSON-RPC calls go through nustd.http.
23
+ memes = [
24
+ "nustd[http]",
25
+ ]
26
+ # The agents demo drives Claude Code through nustd.cc, on the local claude login.
27
+ agents = [
28
+ "nustd[cc]",
29
+ ]
30
+
31
+ [project.urls]
32
+ Repository = "https://github.com/nustackdev/nuverse"
33
+
34
+ [project.entry-points."nuspace.extensions"]
35
+ nudemos = "nudemos:extension"
36
+
37
+ [tool.hatch.build.targets.wheel]
38
+ packages = ["src/nudemos"]
@@ -0,0 +1,43 @@
1
+ """nudemos: the Nu demos as nuspace Planes.
2
+
3
+ - :mod:`.counter`: a count ticking once a second, in its cell's state.
4
+ - :mod:`.sampled`: a kh57 series growing forever, charted from a sample.
5
+ - :mod:`.tour`: the two above, nested under one plane.
6
+ - :mod:`.movies`: a film base, every film and person its own live page.
7
+ - :mod:`.mnist`: a CNN trained on MNIST, steered live, every run tracked. Needs the ``mnist`` extra.
8
+ - :mod:`.memes`: a live terminal over a meme market, synced from Solana.
9
+ - :mod:`.agents`: Claude Code sessions, run, steered and orchestrated from a space.
10
+
11
+ nuspace finds this package through the ``nuspace.extensions`` entry point,
12
+ which names :func:`extension`. Nothing is imported here at module scope.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from typing import TYPE_CHECKING
18
+
19
+
20
+ if TYPE_CHECKING:
21
+ from nuspace import Extension
22
+
23
+
24
+ __all__ = ["extension"]
25
+
26
+
27
+ def extension() -> Extension:
28
+ """The tour and the demo planes, as one extension."""
29
+ from nuspace import Extension
30
+
31
+ from . import agents, counter, memes, mnist, movies, sampled, tour
32
+
33
+ return Extension(
34
+ planes=(
35
+ tour.PLANE,
36
+ counter.PLANE,
37
+ sampled.PLANE,
38
+ movies.PLANE,
39
+ mnist.PLANE,
40
+ memes.PLANE,
41
+ agents.PLANE,
42
+ )
43
+ )
@@ -0,0 +1,44 @@
1
+ """The ``agents`` Plane: Claude Code sessions run, watched, steered and orchestrated from a space.
2
+
3
+ Nu is the control plane, Claude Code does the work. One agent is one Claude
4
+ Code session: a page that streams its transcript, and a headless job under
5
+ the page whose one cell is a short Nu loop over a ``nustd.cc.Session``
6
+ (:mod:`.job`), taking each message from the agent's state and streaming the
7
+ turn back into it. The pages around it only read and steer that state.
8
+
9
+ Orchestration is plain Nu over agents, no framework: a flow is a headless job
10
+ whose cell asks agents, waits on them and reads their answers through a
11
+ :class:`~.flows.Team` (:mod:`.flows`). Three ship, each a plane under this one
12
+ showing its cell in full: Fan-out and judge, Chain, Task board.
13
+
14
+ - :mod:`.claude`, :mod:`.job`: the Claude Code endpoint, and the agent's loop over it.
15
+ - :mod:`.launch`: making an agent, saying something to it, stopping and resuming it.
16
+ - :mod:`.flows`: the :class:`~.flows.Team` terms and the three flows, as cell sources.
17
+ - :mod:`.board`, :mod:`.page`, :mod:`.orchestrate`: the planes, as cell sources.
18
+ - :mod:`.views`, :mod:`.state`: what the cells read and how they shape it.
19
+
20
+ Talking to Claude Code needs the ``claude-agent-sdk`` package (the ``cc`` extra
21
+ of nustd) and a logged-in ``claude`` CLI. No API key: it runs on the login.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ from nuspace import Plane
27
+
28
+ from . import board, orchestrate
29
+
30
+
31
+ __all__ = ["PLANE"]
32
+
33
+
34
+ PLANE = Plane(
35
+ "agents",
36
+ "Agents",
37
+ icon="bot",
38
+ description="Claude Code sessions, run, steered and orchestrated from a space.",
39
+ meta={"editable": True, "full_width": True},
40
+ cells=(("intro", board.INTRO), ("new", board.NEW), ("board", board.BOARD)),
41
+ children=(orchestrate.FANOUT_PLANE, orchestrate.CHAIN_PLANE, orchestrate.TASKS_PLANE),
42
+ group="Demos",
43
+ backend="async",
44
+ )
@@ -0,0 +1,189 @@
1
+ """The Agents plane's own cells: what it is, a form that starts agents, and a card per agent.
2
+
3
+ - ``intro``: what the plane is, and where the flows are.
4
+ - ``new``: name, folder, model, permission mode and a brief, and two buttons:
5
+ Start agent makes one agent page with its job (:func:`~.launch.launch`) and
6
+ opens it; Run the demo starts the Fan-out flow on the built-in scenario,
7
+ three builders and a judge.
8
+ - ``board``: every agent as a card, live: its status, what it is doing, what it cost.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+
14
+ __all__ = ["BOARD", "INTRO", "NEW"]
15
+
16
+
17
+ INTRO = '''\
18
+ import nu
19
+ import nustd.ui
20
+ from nuspace import ops
21
+ from nuspace.system.utils import snap
22
+
23
+ TEXT = """\\
24
+ Claude Code sessions, run, watched, steered and orchestrated from a space, with Nu as the control plane.
25
+
26
+ One agent is one Claude Code session: a page that streams everything it says and does, and a headless job \\
27
+ under it that runs a short Nu loop over the session. Say something and it is the agent's next turn. \\
28
+ Stop it mid-turn, resume it later: it picks up the same conversation, even after the space restarts.
29
+
30
+ Orchestration is a few lines of Nu over agents, each flow a plane with its cell in full: \\
31
+ **Fan-out** (N agents on one brief, a judge picks the best), **Chain** (research, build, review) \\
32
+ and **Task board** (idle agents pull the next task).
33
+ """
34
+
35
+
36
+ class Links(nustd.ui.Row):
37
+ fanout = nustd.ui.LinkRef.slot()
38
+ chain = nustd.ui.LinkRef.slot()
39
+ tasks = nustd.ui.LinkRef.slot()
40
+
41
+
42
+ class View(nustd.ui.Column):
43
+ text = nustd.ui.MarkdownRef.slot()
44
+ links = Links.slot(gap=4)
45
+
46
+
47
+ def links(kids):
48
+ def to(i):
49
+ return nu.Str("/") + nu.Str(nu.List(kids)[i])
50
+
51
+ L = View.links
52
+ return (
53
+ L.fanout.set(href=to(0), label="Fan-out and judge") >> L.chain.set(href=to(1), label="Chain")
54
+ >> L.tasks.set(href=to(2), label="Task board")
55
+ )
56
+
57
+
58
+ def out(plane):
59
+ return View.text.set(TEXT) >> nu.let(snap(ops.children(plane)), links) >> nu.ForeverDo(nu.Delay(3600.0))
60
+ '''
61
+
62
+
63
+ NEW = """\
64
+ import nu
65
+ import nustd.ui
66
+ from nuspace import ops
67
+ from nuspace.system.utils import snap
68
+ from nudemos.agents import views
69
+ from nudemos.agents.claude import READY
70
+ from nudemos.agents.flows import SCENARIO, launch_flow
71
+ from nudemos.agents.launch import launch
72
+ from nudemos.agents.state import agents_of, flows_of, made_by
73
+
74
+
75
+ def pick(options):
76
+ return type("Pick", (nustd.ui.Field,), {"choice": nustd.ui.SelectRef.slot(options=options)})
77
+
78
+
79
+ class Who(nustd.ui.Grid):
80
+ name = nustd.ui.InputRef.slot(label="Name", placeholder="Name it, or leave it to us")
81
+ cwd = nustd.ui.InputRef.slot(label="Working folder", placeholder="A fresh temp folder")
82
+ model = pick(views.MODELS).slot(label="Model")
83
+ mode = pick(views.MODES).slot(label="Permission mode")
84
+
85
+
86
+ class Go(nustd.ui.Row):
87
+ start = nustd.ui.ButtonRef.slot(label="Start agent", icon="play")
88
+ demo = nustd.ui.ButtonRef.slot(label="Run the demo: 3 builders and a judge", variant="secondary", icon="git-fork")
89
+
90
+
91
+ class View(nustd.ui.Column):
92
+ nav = nustd.ui.NavRef.slot()
93
+ notice = nustd.ui.MarkdownRef.slot()
94
+ head = nustd.ui.HeadingRef.slot(level=4)
95
+ who = Who.slot(columns=4, min_width=180, gap=3)
96
+ brief = nustd.ui.TextAreaRef.slot(rows=3, auto_resize=True, placeholder="The brief: what should it do?")
97
+ go = Go.slot(gap=2, align="end")
98
+
99
+
100
+ W = View.who
101
+
102
+
103
+ def started(root):
104
+ # One agent from what the form says, its page opened.
105
+ def made(spec):
106
+ def opened(page):
107
+ return launch(root, spec, page=page) >> views.go(View.nav, nu.Str("/") + nu.Str(page))
108
+
109
+ return nu.let("", opened) >> W.name.set("") >> View.brief.set("")
110
+
111
+ count = snap(agents_of(root).len())
112
+ spec = views.spec_of(
113
+ nu.Str(W.name), nu.Str(W.cwd), nu.Str(W.model.choice), nu.Str(W.mode.choice), nu.Str(View.brief), count
114
+ )
115
+ return nu.let(spec, made)
116
+
117
+
118
+ def demo(root):
119
+ # The built-in scenario: the Fan-out flow, three builders and a judge, on one brief.
120
+ fanout = nu.First(ops.children(root).iter().filter(lambda p: made_by(p) == "agents_fanout"))
121
+
122
+ def run(plane):
123
+ name = nu.Str("Fan-out ") + nu.str(nu.Int(snap(flows_of(plane).len())) + 1)
124
+ spec = nu.Dict.of(brief=SCENARIO, n=3, tasks=[])
125
+ return launch_flow(nu.Str(plane), root, "fanout", spec, name)
126
+
127
+ return nu.let(snap(nu.str(fanout).fallback("")), run)
128
+
129
+
130
+ def defaults():
131
+ return (
132
+ View.head.set("New agent") >> W.name.set("") >> W.cwd.set("")
133
+ >> W.model.choice.set(views.MODELS[0]) >> W.mode.choice.set(views.MODES[0])
134
+ >> View.brief.set("") >> View.go.start.set("Start agent", icon="play")
135
+ >> View.go.demo.set("Run the demo: 3 builders and a judge", variant="secondary", icon="git-fork")
136
+ )
137
+
138
+
139
+ def out(plane):
140
+ if not READY:
141
+ return View.notice.set(views.MISSING) >> nu.ForeverDo(nu.Delay(3600.0))
142
+ root = nu.Str(plane)
143
+ return defaults() >> nu.ParallelAsync(
144
+ nu.ReactForever(View.go.start.on_click(), started(root)),
145
+ nu.ReactForever(View.go.demo.on_click(), demo(root)),
146
+ )
147
+ """
148
+
149
+
150
+ BOARD = """\
151
+ import nu
152
+ import nustd.ui
153
+ from nuspace.shapes import Space
154
+ from nuspace.system.utils import redraws
155
+ from nudemos.agents import views
156
+
157
+
158
+ class AgentCard(nustd.ui.Card):
159
+ pill = nustd.ui.StatusPillRef.slot()
160
+ doing = nustd.ui.TextRef.slot()
161
+ stats = nustd.ui.TextRef.slot()
162
+ open = nustd.ui.LinkRef.slot(label="Open")
163
+
164
+
165
+ class View(nustd.ui.Column):
166
+ head = nustd.ui.HeadingRef.slot(level=4)
167
+ empty = nustd.ui.EmptyStateRef.slot(label="No agents yet", description="Start one above, or run the demo.")
168
+ cards = nustd.ui.GridRef.slot(AgentCard, columns=3, min_width=280, gap=3)
169
+
170
+
171
+ def card(c):
172
+ k = View.cards[c["key"]]
173
+ return (
174
+ k.set_title(c["name"]) >> k.set_subtitle(c["where"]) >> k.pill.set(c["status"], tone=c["tone"])
175
+ >> k.doing.set(c["doing"]) >> k.stats.set(c["stats"]) >> k.open.set(href=c["href"], label="Open")
176
+ )
177
+
178
+
179
+ def draw(cards):
180
+ each = nu.ForEachDo(nu.List(cards), card)
181
+ return nu.IfDo(nu.List(cards).len() > 0, View.empty.erase() >> each)
182
+
183
+
184
+ def out(plane):
185
+ kids = Space.planes[plane].children.on_change()
186
+ return View.head.set("All agents") >> View.empty.set("No agents yet") >> redraws(
187
+ [kids], views.cards(views.board(plane)), draw, every=1.0
188
+ )
189
+ """
@@ -0,0 +1,31 @@
1
+ """The Service an agent's job speaks to Claude Code through.
2
+
3
+ One streamed endpoint over ``nustd.cc``: a call is one turn, its events
4
+ handed over as they happen. Here, in a module, and not in the job's cell,
5
+ so the term that names it can travel to the job's worker process.
6
+
7
+ Importing this needs the ``claude-agent-sdk`` package; the planes check for
8
+ it before they start any agent (:data:`READY`).
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import importlib.util
14
+
15
+ import nu
16
+
17
+
18
+ __all__ = ["READY", "Claude"]
19
+
20
+
21
+ #: Whether the Claude Agent SDK is here to talk to Claude Code with.
22
+ READY = importlib.util.find_spec("claude_agent_sdk") is not None
23
+
24
+
25
+ if READY:
26
+ import nustd.cc
27
+
28
+ class Claude(nu.Service):
29
+ """An agent's one Claude Code endpoint: a turn, streamed."""
30
+
31
+ say = nustd.cc.StreamRef.method()