nuspace 0.3.0__tar.gz → 0.3.1__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.
- {nuspace-0.3.0 → nuspace-0.3.1}/CHANGELOG.md +12 -1
- {nuspace-0.3.0 → nuspace-0.3.1}/PKG-INFO +1 -2
- {nuspace-0.3.0 → nuspace-0.3.1}/pyproject.toml +7 -3
- nuspace-0.3.1/src/nuspace/agent/__init__.py +233 -0
- nuspace-0.3.1/src/nuspace/agent/cc.py +105 -0
- nuspace-0.3.1/src/nuspace/agent/conversation.py +314 -0
- nuspace-0.3.1/src/nuspace/agent/cycles.py +495 -0
- nuspace-0.3.1/src/nuspace/agent/llm.py +87 -0
- nuspace-0.3.1/src/nuspace/agent/panel.py +108 -0
- nuspace-0.3.1/src/nuspace/agent/passes.py +152 -0
- nuspace-0.3.1/src/nuspace/agent/prompt/__init__.py +208 -0
- nuspace-0.3.1/src/nuspace/agent/prompt/catalogue.py +134 -0
- nuspace-0.3.1/src/nuspace/agent/prompt/sections.py +84 -0
- nuspace-0.3.1/src/nuspace/agent/prompt/surface.py +225 -0
- nuspace-0.3.1/src/nuspace/agent/prompt/text/answer.md +15 -0
- nuspace-0.3.1/src/nuspace/agent/prompt/text/crashcourse.md +97 -0
- nuspace-0.3.1/src/nuspace/agent/prompt/text/cycles.md +89 -0
- nuspace-0.3.1/src/nuspace/agent/prompt/text/drawing.md +285 -0
- nuspace-0.3.1/src/nuspace/agent/prompt/text/examples.md +95 -0
- nuspace-0.3.1/src/nuspace/agent/prompt/text/fabrics.md +24 -0
- nuspace-0.3.1/src/nuspace/agent/prompt/text/finish.md +68 -0
- nuspace-0.3.1/src/nuspace/agent/prompt/text/inspect.md +23 -0
- nuspace-0.3.1/src/nuspace/agent/prompt/text/opening.md +6 -0
- nuspace-0.3.1/src/nuspace/agent/prompt/text/protocol.md +122 -0
- nuspace-0.3.1/src/nuspace/agent/prompt/text/role.md +11 -0
- nuspace-0.3.1/src/nuspace/agent/prompt/text/space.md +63 -0
- nuspace-0.3.1/src/nuspace/agent/prompt/text/speaking.md +115 -0
- nuspace-0.3.1/src/nuspace/agent/prompt/text/task.md +21 -0
- nuspace-0.3.1/src/nuspace/agent/prompt/text/thesis.md +131 -0
- nuspace-0.3.1/src/nuspace/agent/session.py +232 -0
- nuspace-0.3.1/src/nuspace/agent/shapes.py +101 -0
- nuspace-0.3.1/src/nuspace/agent/source.py +278 -0
- nuspace-0.3.1/src/nuspace/agent/trace.py +306 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/src/nuspace/drivers/web.py +15 -2
- {nuspace-0.3.0 → nuspace-0.3.1}/src/nuspace/exec/cell.py +23 -7
- {nuspace-0.3.0 → nuspace-0.3.1}/src/nuspace/exec/plane.py +5 -2
- nuspace-0.3.1/src/nuspace/ops/chat.py +770 -0
- nuspace-0.3.1/src/nuspace/ops/groups.py +666 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/src/nuspace/space.py +234 -9
- {nuspace-0.3.0 → nuspace-0.3.1}/src/nuspace/web/session.py +9 -1
- {nuspace-0.3.0 → nuspace-0.3.1}/src/nuspace/web/utils.py +16 -2
- nuspace-0.3.1/tests/test_agent.py +813 -0
- nuspace-0.3.1/tests/test_chat.py +641 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/tests/test_groups.py +63 -3
- nuspace-0.3.0/src/nuspace/ops/chat.py +0 -251
- nuspace-0.3.0/src/nuspace/ops/groups.py +0 -868
- nuspace-0.3.0/tests/test_chat.py +0 -252
- {nuspace-0.3.0 → nuspace-0.3.1}/.github/workflows/publish-nuspace-ui.yml +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/.github/workflows/publish-nuspace.yml +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/.gitignore +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/.pre-commit-config.yaml +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/LICENSE.md +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/Makefile +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/README.md +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/examples/headless.py +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/examples/web.py +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/src/nuspace/__init__.py +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/src/nuspace/cli/__init__.py +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/src/nuspace/cli/__main__.py +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/src/nuspace/cli/cell.py +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/src/nuspace/cli/main.py +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/src/nuspace/cli/plane.py +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/src/nuspace/cli/space.py +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/src/nuspace/cli/utils.py +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/src/nuspace/drivers/__init__.py +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/src/nuspace/drivers/headless.py +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/src/nuspace/exec/__init__.py +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/src/nuspace/exec/space.py +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/src/nuspace/exec/utils.py +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/src/nuspace/ops/__init__.py +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/src/nuspace/ops/cell.py +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/src/nuspace/ops/plane.py +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/src/nuspace/ops/space.py +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/src/nuspace/ops/templates.py +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/src/nuspace/ops/utils.py +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/src/nuspace/presets.py +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/src/nuspace/shapes.py +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/src/nuspace/web/__init__.py +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/src/nuspace/web/route.py +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/src/nuspace/web/shell.py +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/src/nuspace/web/sidebar/__init__.py +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/src/nuspace/web/sidebar/driver.py +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/src/nuspace/web/sidebar/interactions.py +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/src/nuspace/web/sidebar/ref.py +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/src/nuspace/web/viewer/__init__.py +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/src/nuspace/web/viewer/driver.py +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/src/nuspace/web/viewer/interactions.py +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/src/nuspace/web/viewer/ref.py +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/ts/.gitignore +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/ts/biome.json +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/ts/index.html +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/ts/package-lock.json +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/ts/package.json +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/ts/pyproject.toml +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/ts/src/App.tsx +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/ts/src/app/connect.ts +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/ts/src/app/ids.ts +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/ts/src/app/local.ts +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/ts/src/app/router.ts +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/ts/src/app/surfaces.ts +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/ts/src/app/theme.ts +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/ts/src/app/wire.ts +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/ts/src/components/index.ts +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/ts/src/components/section-status-dot.tsx +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/ts/src/design/document.ts +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/ts/src/design/index.ts +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/ts/src/design/rail.ts +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/ts/src/design/section-status.ts +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/ts/src/design/shell.ts +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/ts/src/design/tokens.css +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/ts/src/index.css +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/ts/src/main.tsx +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/ts/src/refs/register.ts +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/ts/src/refs/route.ts +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/ts/src/refs/sidebar/Rail.tsx +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/ts/src/refs/sidebar/footer.tsx +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/ts/src/refs/sidebar/header.tsx +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/ts/src/refs/sidebar/ops.ts +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/ts/src/refs/sidebar/roving-focus.ts +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/ts/src/refs/sidebar/row-input.tsx +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/ts/src/refs/sidebar/row.tsx +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/ts/src/refs/sidebar/sidebar.tsx +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/ts/src/refs/sidebar/state.ts +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/ts/src/refs/sidebar/types.ts +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/ts/src/refs/viewer/Canvas.tsx +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/ts/src/refs/viewer/Code.tsx +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/ts/src/refs/viewer/Program.tsx +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/ts/src/refs/viewer/ProseRef.test.tsx +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/ts/src/refs/viewer/ProseRef.tsx +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/ts/src/refs/viewer/Slash.tsx +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/ts/src/refs/viewer/blocks.ts +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/ts/src/refs/viewer/monaco-impl.ts +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/ts/src/refs/viewer/monaco.ts +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/ts/src/refs/viewer/ops.ts +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/ts/src/refs/viewer/prose.ts +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/ts/src/refs/viewer/state.ts +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/ts/src/refs/viewer/types.ts +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/ts/src/refs/viewer/viewer.tsx +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/ts/src/vite-env.d.ts +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/ts/tsconfig.app.json +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/ts/tsconfig.json +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/ts/tsconfig.node.json +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/ts/vite.config.ts +0 -0
- {nuspace-0.3.0 → nuspace-0.3.1}/ts/vitest.config.ts +0 -0
|
@@ -2,13 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
## Packages shipped from this repo
|
|
4
4
|
|
|
5
|
-
- **nuspace** (the runtime, the `nuspace` command) — 0.3.
|
|
5
|
+
- **nuspace** (the runtime, the `nuspace` command) — 0.3.1
|
|
6
6
|
- **nuspace-ui** (the compiled web bundle) — 0.3.0
|
|
7
7
|
|
|
8
8
|
Below is the changelog for **nuspace** - the full commit stream. Newest first.
|
|
9
9
|
The `nuspace-ui` wheel is the same source tree's vite output and is not tracked
|
|
10
10
|
separately here.
|
|
11
11
|
|
|
12
|
+
## 0.3.1 — 2026-09-23
|
|
13
|
+
|
|
14
|
+
- Opening a page no longer slows down with each extra cell on it
|
|
15
|
+
- Navigating away clears the old page at once instead of blinking through it
|
|
16
|
+
- Keep workers ready in advance, so opening a plane no longer waits for one to start
|
|
17
|
+
- Draw a page without a web server loaded behind it, so it opens much sooner
|
|
18
|
+
- Let a chat work as long as the work takes, and stop it only when it is stuck
|
|
19
|
+
- Always get a way to say something else, whatever a turn drew
|
|
20
|
+
- Watch a chat work step by step, and never get a broken answer
|
|
21
|
+
- Ask a chat for something and it draws the answer and how you reply
|
|
22
|
+
|
|
12
23
|
## 0.3.0 — 2026-09-21
|
|
13
24
|
|
|
14
25
|
- Make a chat, ask it for something, and watch it work the space
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: nuspace
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: Nu runtime built on Nu. A self-hosted programmable space that runs Nu apps in a tree of containers and applications.
|
|
5
5
|
Project-URL: Repository, https://github.com/nustackdev/nuspace
|
|
6
6
|
Author-email: Gor Arakelyan <gorarkln@gmail.com>
|
|
@@ -16,7 +16,6 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
16
16
|
Classifier: Programming Language :: Python :: 3.13
|
|
17
17
|
Requires-Python: >=3.10
|
|
18
18
|
Requires-Dist: click>=8.0
|
|
19
|
-
Requires-Dist: nuagent>=0.1.3
|
|
20
19
|
Requires-Dist: nucore>=0.5.2
|
|
21
20
|
Requires-Dist: nuspace-ui>=0.3.0
|
|
22
21
|
Requires-Dist: nustd[all]>=0.5.2
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "nuspace"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.1"
|
|
8
8
|
description = "Nu runtime built on Nu. A self-hosted programmable space that runs Nu apps in a tree of containers and applications."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "AGPL-3.0-or-later"
|
|
@@ -22,7 +22,6 @@ dependencies = [
|
|
|
22
22
|
# project size and a SPA rebuilt on every UI tweak would eat the quota --
|
|
23
23
|
# not optional. `pip install nuspace` gives you the browser.
|
|
24
24
|
"nuspace-ui>=0.3.0",
|
|
25
|
-
"nuagent>=0.1.3",
|
|
26
25
|
"typing-extensions>=4.7.0",
|
|
27
26
|
"click>=8.0",
|
|
28
27
|
"rich>=10.7",
|
|
@@ -55,6 +54,12 @@ Repository = "https://github.com/nustackdev/nuspace"
|
|
|
55
54
|
nuspace = "nuspace.cli:main"
|
|
56
55
|
|
|
57
56
|
[tool.hatch.build.targets.wheel]
|
|
57
|
+
# Whole directories, not python files: the agent's system prompt is prose in
|
|
58
|
+
# `nuspace/agent/prompt/text/*.md`, read at runtime through
|
|
59
|
+
# importlib.resources. hatchling ships every file under a listed package, so
|
|
60
|
+
# this needs no data entry of its own -- but adding an `include` or
|
|
61
|
+
# `only-include` here would drop the prose and leave a wheel whose chats
|
|
62
|
+
# raise on their first message. Don't.
|
|
58
63
|
packages = ["src/nuspace"]
|
|
59
64
|
|
|
60
65
|
[tool.hatch.build.targets.sdist]
|
|
@@ -68,7 +73,6 @@ nuspace-ui = { path = "./ts" }
|
|
|
68
73
|
nucore = { path = "../nu", editable = true }
|
|
69
74
|
nustd = { path = "../nu/pkgs/nustd", editable = true }
|
|
70
75
|
nucli = { path = "../nu/pkgs/nucli", editable = true }
|
|
71
|
-
nuagent = { path = "../nuagent", editable = true }
|
|
72
76
|
|
|
73
77
|
[tool.uv]
|
|
74
78
|
default-groups = ["dev"]
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"""The agent a chat runs: it answers by drawing, and the host checks the drawing.
|
|
2
|
+
|
|
3
|
+
A chat is two Planes. One runs, and the Cell on it is this package. One draws,
|
|
4
|
+
and what is on it is whatever this turn and the turns before it put there.
|
|
5
|
+
Every turn leaves three Cells on the Plane that draws. Two of them are the
|
|
6
|
+
host's: the panel, appended the moment somebody presses send, and the escape
|
|
7
|
+
hatch, a folded box appended under the answer so that a person always has a
|
|
8
|
+
way to say something. The third is the answer itself, which is the response
|
|
9
|
+
and the next input form in one. The reply box is not chrome any more, it is a
|
|
10
|
+
Cell a model wrote, so it can just as well be three buttons, a form, a slider,
|
|
11
|
+
or a diff with approve and reject on it.
|
|
12
|
+
|
|
13
|
+
What stays uniform is the **record**. Whatever the person does through whatever
|
|
14
|
+
was drawn lands in the conversation as an ordinary ``{role, text}`` message,
|
|
15
|
+
and that list is what wakes the agent for the next turn. Cells are
|
|
16
|
+
presentation; the conversation is the truth.
|
|
17
|
+
|
|
18
|
+
That is natural here and awkward anywhere else, because a Cell's program is
|
|
19
|
+
Nu: what the model generates is a live program bound to the store rather than
|
|
20
|
+
a dead render of a payload. It also persists as an ordinary Cell, so a person
|
|
21
|
+
can open what the agent made and edit it.
|
|
22
|
+
|
|
23
|
+
The structure, and every word of it is load bearing::
|
|
24
|
+
|
|
25
|
+
chat the conversation. one input runs a turn
|
|
26
|
+
turn one input, looped until done
|
|
27
|
+
cycle a phase of the turn. exactly two: work, then answer
|
|
28
|
+
pass one model call plus the program it wrote
|
|
29
|
+
|
|
30
|
+
The modules::
|
|
31
|
+
|
|
32
|
+
conversation the chat loop and the turn: heard, work, answer
|
|
33
|
+
cycles the two cycles, and the check before an answer lands
|
|
34
|
+
passes one pass, composed, with the host's rows between the links
|
|
35
|
+
trace those rows: the fixed machine, and where it is written
|
|
36
|
+
session a turn's working memory, in the store, under the chat
|
|
37
|
+
shapes the slots it runs on, and the one slot the model writes
|
|
38
|
+
source fenced extraction, one attempt, and the Cell check
|
|
39
|
+
panel display(): the trace of one turn, hardcoded, drawn live
|
|
40
|
+
cc Claude Code. one session for as long as the chat
|
|
41
|
+
llm a served model over the OpenAI wire. one call per pass
|
|
42
|
+
prompt what the model reads, as markdown on disk
|
|
43
|
+
|
|
44
|
+
**The pass is composed here rather than taken from nuagent, and that is the
|
|
45
|
+
whole point of the move.** nuagent's ``turn`` is one opaque composed Flow, so
|
|
46
|
+
there is nowhere to stand between the model call going out and the program
|
|
47
|
+
coming back, and a chat wearing it showed a person a blank panel for a minute
|
|
48
|
+
and then everything at once. Composing it here buys six places to stand.
|
|
49
|
+
nuagent stays where it is and keeps working; we stopped importing it.
|
|
50
|
+
|
|
51
|
+
**Two entry points onto one core.** :func:`converse` is a chat: two cycles,
|
|
52
|
+
a conversation, a drawn answer. :func:`perform` is a job agent: the work cycle
|
|
53
|
+
with a fixed task and no drawing. Splitting them later is cheap and nothing
|
|
54
|
+
about the core would have to move.
|
|
55
|
+
|
|
56
|
+
**Three names are not imported until somebody asks for one**, and that is not
|
|
57
|
+
tidiness, it is what the two kinds of Cell in here cost each other. The Cell
|
|
58
|
+
that draws a chat's panel wants :func:`display` and no model; the Cell that
|
|
59
|
+
talks wants an endpoint and draws nothing. Imported eagerly, each pays for the
|
|
60
|
+
other: ``nustd.cc`` pulls the Claude Code SDK, which is most of a second, and
|
|
61
|
+
``nuspace.agent.panel`` pulls ``nustd.ui``, which is a third of one. A panel
|
|
62
|
+
Cell is launched in a fresh worker every time somebody navigates to a chat, so
|
|
63
|
+
that is a third of a second of Claude Code SDK on every navigation for a Cell
|
|
64
|
+
that will never speak. :data:`DEFERRED` is the table and the module
|
|
65
|
+
``__getattr__`` below is the whole of the mechanism.
|
|
66
|
+
|
|
67
|
+
**This package is where the behaviour lives, not the template.** A chat's
|
|
68
|
+
talking Cell holds ten lines that call :func:`converse`, so installing a newer
|
|
69
|
+
nuspace upgrades every chat already in the store. Templates are seeds and a
|
|
70
|
+
seed has no migration; for a prompt that gets tuned daily, seeding is the
|
|
71
|
+
wrong shape.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
from __future__ import annotations
|
|
75
|
+
|
|
76
|
+
from importlib import import_module
|
|
77
|
+
from typing import TYPE_CHECKING
|
|
78
|
+
|
|
79
|
+
import nustd.kv
|
|
80
|
+
from nuspace.agent import conversation, cycles, passes, prompt, session, source, trace
|
|
81
|
+
from nuspace.agent.shapes import Run, Session
|
|
82
|
+
from nuspace.shapes import Space
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
if TYPE_CHECKING:
|
|
86
|
+
from collections.abc import Callable
|
|
87
|
+
|
|
88
|
+
import nu
|
|
89
|
+
from nuspace.agent.panel import display
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
__all__ = [
|
|
93
|
+
"DEFERRED",
|
|
94
|
+
"Run",
|
|
95
|
+
"Session",
|
|
96
|
+
"claude_code",
|
|
97
|
+
"conversation",
|
|
98
|
+
"converse",
|
|
99
|
+
"cycles",
|
|
100
|
+
"display",
|
|
101
|
+
"passes",
|
|
102
|
+
"perform",
|
|
103
|
+
"prompt",
|
|
104
|
+
"served_model",
|
|
105
|
+
"session",
|
|
106
|
+
"source",
|
|
107
|
+
"trace",
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
#: The names that cost an import nobody else here needs, by the module each
|
|
112
|
+
#: lives in. Read by :func:`__getattr__`, so ``nuspace.agent.display`` still
|
|
113
|
+
#: resolves and the import it costs is paid by whoever asked for it.
|
|
114
|
+
DEFERRED = {"claude_code": "cc", "display": "panel", "served_model": "llm"}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def __getattr__(name: str) -> object:
|
|
118
|
+
"""Resolve one of :data:`DEFERRED` by importing the module it lives in.
|
|
119
|
+
|
|
120
|
+
Python calls this only for names the module does not already have, so it
|
|
121
|
+
costs nothing on any other attribute, and the result is cached by the
|
|
122
|
+
ordinary import machinery rather than by anything here.
|
|
123
|
+
"""
|
|
124
|
+
if name in DEFERRED:
|
|
125
|
+
return getattr(import_module(f"{__name__}.{DEFERRED[name]}"), name)
|
|
126
|
+
msg = f"module {__name__!r} has no attribute {name!r}"
|
|
127
|
+
raise AttributeError(msg)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def converse(
|
|
131
|
+
plane_id: nu.StrArg,
|
|
132
|
+
cell_id: nu.StrArg,
|
|
133
|
+
*,
|
|
134
|
+
ui_plane_id: nu.StrArg,
|
|
135
|
+
root: type[Space] = Space,
|
|
136
|
+
talk: Callable[..., nu.Nu] | None = None,
|
|
137
|
+
) -> nu.Nu:
|
|
138
|
+
"""The whole term a chat's talking Cell runs.
|
|
139
|
+
|
|
140
|
+
Answer whatever is outstanding, then wait for more, forever, with the
|
|
141
|
+
endpoint held open around all of it.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
plane_id: the Plane the chat runs on. The Cell's own ``plane``.
|
|
145
|
+
cell_id: the Cell on it that talks and holds the conversation. The
|
|
146
|
+
Cell's own ``cell``.
|
|
147
|
+
ui_plane_id: the Plane the agent draws its answers onto. The other
|
|
148
|
+
half of the pair a chat is, and the one the person is looking at.
|
|
149
|
+
root: the Space shape class. It decides the store the agent writes to
|
|
150
|
+
and the surface it is shown, so a subclassed Space gets a prompt
|
|
151
|
+
about itself.
|
|
152
|
+
talk: the endpoint, ``endpoint(loop, *, system)``. ``None`` means
|
|
153
|
+
Claude Code with its defaults, which is what a chat made by
|
|
154
|
+
pressing ``+`` gets. Pass
|
|
155
|
+
:func:`~nuspace.agent.llm.served_model` or
|
|
156
|
+
:func:`~nuspace.agent.cc.claude_code` with arguments of your own
|
|
157
|
+
for anything else.
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
A Flow that never ends on its own.
|
|
161
|
+
|
|
162
|
+
Notes:
|
|
163
|
+
- The bracket over the store is here rather than in the template,
|
|
164
|
+
because a program owns its own atomicity and this *is* the program.
|
|
165
|
+
Nothing brackets it on the way in: the host cannot see inside a
|
|
166
|
+
program it evaluates.
|
|
167
|
+
- The endpoint goes up once, outside the wait, so a session lasts as
|
|
168
|
+
long as the chat rather than as long as a turn.
|
|
169
|
+
"""
|
|
170
|
+
|
|
171
|
+
def loop(ask: Callable[..., nu.Nu]) -> nu.Nu:
|
|
172
|
+
return conversation.answering(
|
|
173
|
+
plane_id, cell_id, ui_plane_id=ui_plane_id, ask=ask, root=root
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
return _under(talk, loop, prompt.system_prompt(root=root), root)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def perform(
|
|
180
|
+
plane_id: nu.StrArg,
|
|
181
|
+
cell_id: nu.StrArg,
|
|
182
|
+
task: str,
|
|
183
|
+
*,
|
|
184
|
+
root: type[Space] = Space,
|
|
185
|
+
talk: Callable[..., nu.Nu] | None = None,
|
|
186
|
+
) -> nu.Nu:
|
|
187
|
+
"""One job agent: the work cycle, a fixed task, and no drawing.
|
|
188
|
+
|
|
189
|
+
The narrow half of the same core. Nobody is talking to a job agent, so
|
|
190
|
+
there is nothing to hear and nothing to answer, and what is left is the
|
|
191
|
+
cycle that changes the space. It narrates into its own Cell's state, so
|
|
192
|
+
:func:`display` over that Cell shows a job doing its work exactly the way
|
|
193
|
+
it shows a chat doing a turn.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
plane_id: the Plane the job runs on. The Cell's own ``plane``.
|
|
197
|
+
cell_id: the Cell doing the work. The Cell's own ``cell``.
|
|
198
|
+
task: what to do, in prose. It goes in the prompt and again as the
|
|
199
|
+
first message, because a prompt is what a model is told once and
|
|
200
|
+
a message is what it is looking at.
|
|
201
|
+
root: the Space shape class.
|
|
202
|
+
talk: the endpoint, as for :func:`converse`.
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
A Flow that runs the work cycle once and ends.
|
|
206
|
+
"""
|
|
207
|
+
|
|
208
|
+
def once(ask: Callable[..., nu.Nu]) -> nu.Nu:
|
|
209
|
+
return conversation.performing(plane_id, cell_id, task, ask=ask, root=root)
|
|
210
|
+
|
|
211
|
+
system = prompt.system_prompt(root=root, task=task, drawing=False)
|
|
212
|
+
return _under(talk, once, system, root)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _under(
|
|
216
|
+
talk: Callable[..., nu.Nu] | None,
|
|
217
|
+
body: Callable[..., nu.Nu],
|
|
218
|
+
system: str,
|
|
219
|
+
root: type[Space],
|
|
220
|
+
) -> nu.Nu:
|
|
221
|
+
"""``body`` with an endpoint up around it and the store bracketed once.
|
|
222
|
+
|
|
223
|
+
Claude Code is imported here rather than at the top of the module, so a
|
|
224
|
+
Cell that reads this package for :func:`display` and talks to no model
|
|
225
|
+
never pays for the SDK.
|
|
226
|
+
"""
|
|
227
|
+
if talk is None:
|
|
228
|
+
from nuspace.agent.cc import claude_code
|
|
229
|
+
|
|
230
|
+
endpoint = claude_code()
|
|
231
|
+
else:
|
|
232
|
+
endpoint = talk
|
|
233
|
+
return nustd.kv.auto_flow_atomic(endpoint(body, system=system), scope=root)
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Claude Code, and one session for as long as the chat is up.
|
|
2
|
+
|
|
3
|
+
A space is a thing you run on the machine you are sitting at, so the endpoint
|
|
4
|
+
a chat talks to by default is the Claude Code already installed on it rather
|
|
5
|
+
than an api key somebody has to find.
|
|
6
|
+
|
|
7
|
+
**The session is the whole point of this file.** ``nustd.cc.Session`` is a
|
|
8
|
+
bracket: the first prompt inside it starts a conversation, every later prompt
|
|
9
|
+
resumes it. Opened around the chat's whole loop rather than around a pass, so
|
|
10
|
+
the model keeps its own context from one pass of a turn to the next and a
|
|
11
|
+
prompt carries only what is new. Without it every call is a cold start, the
|
|
12
|
+
entire transcript has to ride in every prompt, and pass eight re-sends seven
|
|
13
|
+
passes of code and diagnostics to be told one line.
|
|
14
|
+
|
|
15
|
+
So :func:`ask` sends the newest message and nothing before it. That is right
|
|
16
|
+
cold as well as warm: the first message of a turn is the opening, which
|
|
17
|
+
carries the conversation whole, and every message after it is an observation
|
|
18
|
+
about the pass that just ran.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from typing import TYPE_CHECKING
|
|
24
|
+
|
|
25
|
+
import nu
|
|
26
|
+
import nustd.cc
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
if TYPE_CHECKING:
|
|
30
|
+
from collections.abc import Callable, Sequence
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
__all__ = ["MODEL", "Bot", "ask", "claude_code"]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
#: What a chat talks to when nobody says otherwise.
|
|
37
|
+
MODEL = "claude-opus-5"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Bot(nu.Service):
|
|
41
|
+
"""The Claude Code endpoint a chat runs against."""
|
|
42
|
+
|
|
43
|
+
ask = nustd.cc.PromptRef.method()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def ask(*, messages: nu.Nu) -> nu.Nu:
|
|
47
|
+
"""One pass's prompt: the newest message, and nothing before it.
|
|
48
|
+
|
|
49
|
+
What one pass calls. It hands over the whole transcript and this
|
|
50
|
+
takes the last of it, because the rest is already in the session.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
messages: the working memory's message list, oldest first.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
The prompt interaction, yielding a dict with ``text`` in it.
|
|
57
|
+
"""
|
|
58
|
+
newest = nu.Dict(nu.List(messages)[nu.Int(-1)])
|
|
59
|
+
return Bot.ask(prompt=nu.ToStr(newest.get_item(nu.Str("content"), nu.Str(""))))
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def claude_code(
|
|
63
|
+
*,
|
|
64
|
+
model: str = MODEL,
|
|
65
|
+
allowed_tools: Sequence[str] = (),
|
|
66
|
+
permission_mode: str = "default",
|
|
67
|
+
) -> Callable[..., nu.Nu]:
|
|
68
|
+
"""The endpoint a chat runs against: Claude Code, in one session.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
model: which model to ask.
|
|
72
|
+
allowed_tools: the tools Claude Code may use. Empty on purpose: the
|
|
73
|
+
model's action here is the Nu program it writes, and a model that
|
|
74
|
+
can edit files as well has two ways to change the world and only
|
|
75
|
+
one of them is in the transcript.
|
|
76
|
+
permission_mode: what Claude Code does when something needs
|
|
77
|
+
permission. Nobody is at the terminal to answer, so this only
|
|
78
|
+
matters once tools are allowed.
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
A callable ``endpoint(loop, *, system)``. ``loop`` is what to run
|
|
82
|
+
once the endpoint is up, as a function of :func:`ask`; ``system`` is
|
|
83
|
+
the system prompt. It yields whatever ``loop`` yields.
|
|
84
|
+
|
|
85
|
+
Notes:
|
|
86
|
+
- The bracket is the lifetime. The client opens when the ``With`` is
|
|
87
|
+
entered and closes when it exits, so everything the chat will ever
|
|
88
|
+
do has to be inside it.
|
|
89
|
+
- Reach is by Context rather than by ownership, so a prompt inside a
|
|
90
|
+
nested ``With`` the loop opens still joins the session.
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
def endpoint(loop: Callable[..., nu.Nu], *, system: str) -> nu.Nu:
|
|
94
|
+
return nu.With(
|
|
95
|
+
nustd.cc.bind(
|
|
96
|
+
Bot,
|
|
97
|
+
model=model,
|
|
98
|
+
system_prompt=system,
|
|
99
|
+
allowed_tools=list(allowed_tools),
|
|
100
|
+
permission_mode=permission_mode,
|
|
101
|
+
),
|
|
102
|
+
body=nustd.cc.Session(loop(ask)),
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
return endpoint
|