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.
Files changed (144) hide show
  1. {nuspace-0.3.0 → nuspace-0.3.1}/CHANGELOG.md +12 -1
  2. {nuspace-0.3.0 → nuspace-0.3.1}/PKG-INFO +1 -2
  3. {nuspace-0.3.0 → nuspace-0.3.1}/pyproject.toml +7 -3
  4. nuspace-0.3.1/src/nuspace/agent/__init__.py +233 -0
  5. nuspace-0.3.1/src/nuspace/agent/cc.py +105 -0
  6. nuspace-0.3.1/src/nuspace/agent/conversation.py +314 -0
  7. nuspace-0.3.1/src/nuspace/agent/cycles.py +495 -0
  8. nuspace-0.3.1/src/nuspace/agent/llm.py +87 -0
  9. nuspace-0.3.1/src/nuspace/agent/panel.py +108 -0
  10. nuspace-0.3.1/src/nuspace/agent/passes.py +152 -0
  11. nuspace-0.3.1/src/nuspace/agent/prompt/__init__.py +208 -0
  12. nuspace-0.3.1/src/nuspace/agent/prompt/catalogue.py +134 -0
  13. nuspace-0.3.1/src/nuspace/agent/prompt/sections.py +84 -0
  14. nuspace-0.3.1/src/nuspace/agent/prompt/surface.py +225 -0
  15. nuspace-0.3.1/src/nuspace/agent/prompt/text/answer.md +15 -0
  16. nuspace-0.3.1/src/nuspace/agent/prompt/text/crashcourse.md +97 -0
  17. nuspace-0.3.1/src/nuspace/agent/prompt/text/cycles.md +89 -0
  18. nuspace-0.3.1/src/nuspace/agent/prompt/text/drawing.md +285 -0
  19. nuspace-0.3.1/src/nuspace/agent/prompt/text/examples.md +95 -0
  20. nuspace-0.3.1/src/nuspace/agent/prompt/text/fabrics.md +24 -0
  21. nuspace-0.3.1/src/nuspace/agent/prompt/text/finish.md +68 -0
  22. nuspace-0.3.1/src/nuspace/agent/prompt/text/inspect.md +23 -0
  23. nuspace-0.3.1/src/nuspace/agent/prompt/text/opening.md +6 -0
  24. nuspace-0.3.1/src/nuspace/agent/prompt/text/protocol.md +122 -0
  25. nuspace-0.3.1/src/nuspace/agent/prompt/text/role.md +11 -0
  26. nuspace-0.3.1/src/nuspace/agent/prompt/text/space.md +63 -0
  27. nuspace-0.3.1/src/nuspace/agent/prompt/text/speaking.md +115 -0
  28. nuspace-0.3.1/src/nuspace/agent/prompt/text/task.md +21 -0
  29. nuspace-0.3.1/src/nuspace/agent/prompt/text/thesis.md +131 -0
  30. nuspace-0.3.1/src/nuspace/agent/session.py +232 -0
  31. nuspace-0.3.1/src/nuspace/agent/shapes.py +101 -0
  32. nuspace-0.3.1/src/nuspace/agent/source.py +278 -0
  33. nuspace-0.3.1/src/nuspace/agent/trace.py +306 -0
  34. {nuspace-0.3.0 → nuspace-0.3.1}/src/nuspace/drivers/web.py +15 -2
  35. {nuspace-0.3.0 → nuspace-0.3.1}/src/nuspace/exec/cell.py +23 -7
  36. {nuspace-0.3.0 → nuspace-0.3.1}/src/nuspace/exec/plane.py +5 -2
  37. nuspace-0.3.1/src/nuspace/ops/chat.py +770 -0
  38. nuspace-0.3.1/src/nuspace/ops/groups.py +666 -0
  39. {nuspace-0.3.0 → nuspace-0.3.1}/src/nuspace/space.py +234 -9
  40. {nuspace-0.3.0 → nuspace-0.3.1}/src/nuspace/web/session.py +9 -1
  41. {nuspace-0.3.0 → nuspace-0.3.1}/src/nuspace/web/utils.py +16 -2
  42. nuspace-0.3.1/tests/test_agent.py +813 -0
  43. nuspace-0.3.1/tests/test_chat.py +641 -0
  44. {nuspace-0.3.0 → nuspace-0.3.1}/tests/test_groups.py +63 -3
  45. nuspace-0.3.0/src/nuspace/ops/chat.py +0 -251
  46. nuspace-0.3.0/src/nuspace/ops/groups.py +0 -868
  47. nuspace-0.3.0/tests/test_chat.py +0 -252
  48. {nuspace-0.3.0 → nuspace-0.3.1}/.github/workflows/publish-nuspace-ui.yml +0 -0
  49. {nuspace-0.3.0 → nuspace-0.3.1}/.github/workflows/publish-nuspace.yml +0 -0
  50. {nuspace-0.3.0 → nuspace-0.3.1}/.gitignore +0 -0
  51. {nuspace-0.3.0 → nuspace-0.3.1}/.pre-commit-config.yaml +0 -0
  52. {nuspace-0.3.0 → nuspace-0.3.1}/LICENSE.md +0 -0
  53. {nuspace-0.3.0 → nuspace-0.3.1}/Makefile +0 -0
  54. {nuspace-0.3.0 → nuspace-0.3.1}/README.md +0 -0
  55. {nuspace-0.3.0 → nuspace-0.3.1}/examples/headless.py +0 -0
  56. {nuspace-0.3.0 → nuspace-0.3.1}/examples/web.py +0 -0
  57. {nuspace-0.3.0 → nuspace-0.3.1}/src/nuspace/__init__.py +0 -0
  58. {nuspace-0.3.0 → nuspace-0.3.1}/src/nuspace/cli/__init__.py +0 -0
  59. {nuspace-0.3.0 → nuspace-0.3.1}/src/nuspace/cli/__main__.py +0 -0
  60. {nuspace-0.3.0 → nuspace-0.3.1}/src/nuspace/cli/cell.py +0 -0
  61. {nuspace-0.3.0 → nuspace-0.3.1}/src/nuspace/cli/main.py +0 -0
  62. {nuspace-0.3.0 → nuspace-0.3.1}/src/nuspace/cli/plane.py +0 -0
  63. {nuspace-0.3.0 → nuspace-0.3.1}/src/nuspace/cli/space.py +0 -0
  64. {nuspace-0.3.0 → nuspace-0.3.1}/src/nuspace/cli/utils.py +0 -0
  65. {nuspace-0.3.0 → nuspace-0.3.1}/src/nuspace/drivers/__init__.py +0 -0
  66. {nuspace-0.3.0 → nuspace-0.3.1}/src/nuspace/drivers/headless.py +0 -0
  67. {nuspace-0.3.0 → nuspace-0.3.1}/src/nuspace/exec/__init__.py +0 -0
  68. {nuspace-0.3.0 → nuspace-0.3.1}/src/nuspace/exec/space.py +0 -0
  69. {nuspace-0.3.0 → nuspace-0.3.1}/src/nuspace/exec/utils.py +0 -0
  70. {nuspace-0.3.0 → nuspace-0.3.1}/src/nuspace/ops/__init__.py +0 -0
  71. {nuspace-0.3.0 → nuspace-0.3.1}/src/nuspace/ops/cell.py +0 -0
  72. {nuspace-0.3.0 → nuspace-0.3.1}/src/nuspace/ops/plane.py +0 -0
  73. {nuspace-0.3.0 → nuspace-0.3.1}/src/nuspace/ops/space.py +0 -0
  74. {nuspace-0.3.0 → nuspace-0.3.1}/src/nuspace/ops/templates.py +0 -0
  75. {nuspace-0.3.0 → nuspace-0.3.1}/src/nuspace/ops/utils.py +0 -0
  76. {nuspace-0.3.0 → nuspace-0.3.1}/src/nuspace/presets.py +0 -0
  77. {nuspace-0.3.0 → nuspace-0.3.1}/src/nuspace/shapes.py +0 -0
  78. {nuspace-0.3.0 → nuspace-0.3.1}/src/nuspace/web/__init__.py +0 -0
  79. {nuspace-0.3.0 → nuspace-0.3.1}/src/nuspace/web/route.py +0 -0
  80. {nuspace-0.3.0 → nuspace-0.3.1}/src/nuspace/web/shell.py +0 -0
  81. {nuspace-0.3.0 → nuspace-0.3.1}/src/nuspace/web/sidebar/__init__.py +0 -0
  82. {nuspace-0.3.0 → nuspace-0.3.1}/src/nuspace/web/sidebar/driver.py +0 -0
  83. {nuspace-0.3.0 → nuspace-0.3.1}/src/nuspace/web/sidebar/interactions.py +0 -0
  84. {nuspace-0.3.0 → nuspace-0.3.1}/src/nuspace/web/sidebar/ref.py +0 -0
  85. {nuspace-0.3.0 → nuspace-0.3.1}/src/nuspace/web/viewer/__init__.py +0 -0
  86. {nuspace-0.3.0 → nuspace-0.3.1}/src/nuspace/web/viewer/driver.py +0 -0
  87. {nuspace-0.3.0 → nuspace-0.3.1}/src/nuspace/web/viewer/interactions.py +0 -0
  88. {nuspace-0.3.0 → nuspace-0.3.1}/src/nuspace/web/viewer/ref.py +0 -0
  89. {nuspace-0.3.0 → nuspace-0.3.1}/ts/.gitignore +0 -0
  90. {nuspace-0.3.0 → nuspace-0.3.1}/ts/biome.json +0 -0
  91. {nuspace-0.3.0 → nuspace-0.3.1}/ts/index.html +0 -0
  92. {nuspace-0.3.0 → nuspace-0.3.1}/ts/package-lock.json +0 -0
  93. {nuspace-0.3.0 → nuspace-0.3.1}/ts/package.json +0 -0
  94. {nuspace-0.3.0 → nuspace-0.3.1}/ts/pyproject.toml +0 -0
  95. {nuspace-0.3.0 → nuspace-0.3.1}/ts/src/App.tsx +0 -0
  96. {nuspace-0.3.0 → nuspace-0.3.1}/ts/src/app/connect.ts +0 -0
  97. {nuspace-0.3.0 → nuspace-0.3.1}/ts/src/app/ids.ts +0 -0
  98. {nuspace-0.3.0 → nuspace-0.3.1}/ts/src/app/local.ts +0 -0
  99. {nuspace-0.3.0 → nuspace-0.3.1}/ts/src/app/router.ts +0 -0
  100. {nuspace-0.3.0 → nuspace-0.3.1}/ts/src/app/surfaces.ts +0 -0
  101. {nuspace-0.3.0 → nuspace-0.3.1}/ts/src/app/theme.ts +0 -0
  102. {nuspace-0.3.0 → nuspace-0.3.1}/ts/src/app/wire.ts +0 -0
  103. {nuspace-0.3.0 → nuspace-0.3.1}/ts/src/components/index.ts +0 -0
  104. {nuspace-0.3.0 → nuspace-0.3.1}/ts/src/components/section-status-dot.tsx +0 -0
  105. {nuspace-0.3.0 → nuspace-0.3.1}/ts/src/design/document.ts +0 -0
  106. {nuspace-0.3.0 → nuspace-0.3.1}/ts/src/design/index.ts +0 -0
  107. {nuspace-0.3.0 → nuspace-0.3.1}/ts/src/design/rail.ts +0 -0
  108. {nuspace-0.3.0 → nuspace-0.3.1}/ts/src/design/section-status.ts +0 -0
  109. {nuspace-0.3.0 → nuspace-0.3.1}/ts/src/design/shell.ts +0 -0
  110. {nuspace-0.3.0 → nuspace-0.3.1}/ts/src/design/tokens.css +0 -0
  111. {nuspace-0.3.0 → nuspace-0.3.1}/ts/src/index.css +0 -0
  112. {nuspace-0.3.0 → nuspace-0.3.1}/ts/src/main.tsx +0 -0
  113. {nuspace-0.3.0 → nuspace-0.3.1}/ts/src/refs/register.ts +0 -0
  114. {nuspace-0.3.0 → nuspace-0.3.1}/ts/src/refs/route.ts +0 -0
  115. {nuspace-0.3.0 → nuspace-0.3.1}/ts/src/refs/sidebar/Rail.tsx +0 -0
  116. {nuspace-0.3.0 → nuspace-0.3.1}/ts/src/refs/sidebar/footer.tsx +0 -0
  117. {nuspace-0.3.0 → nuspace-0.3.1}/ts/src/refs/sidebar/header.tsx +0 -0
  118. {nuspace-0.3.0 → nuspace-0.3.1}/ts/src/refs/sidebar/ops.ts +0 -0
  119. {nuspace-0.3.0 → nuspace-0.3.1}/ts/src/refs/sidebar/roving-focus.ts +0 -0
  120. {nuspace-0.3.0 → nuspace-0.3.1}/ts/src/refs/sidebar/row-input.tsx +0 -0
  121. {nuspace-0.3.0 → nuspace-0.3.1}/ts/src/refs/sidebar/row.tsx +0 -0
  122. {nuspace-0.3.0 → nuspace-0.3.1}/ts/src/refs/sidebar/sidebar.tsx +0 -0
  123. {nuspace-0.3.0 → nuspace-0.3.1}/ts/src/refs/sidebar/state.ts +0 -0
  124. {nuspace-0.3.0 → nuspace-0.3.1}/ts/src/refs/sidebar/types.ts +0 -0
  125. {nuspace-0.3.0 → nuspace-0.3.1}/ts/src/refs/viewer/Canvas.tsx +0 -0
  126. {nuspace-0.3.0 → nuspace-0.3.1}/ts/src/refs/viewer/Code.tsx +0 -0
  127. {nuspace-0.3.0 → nuspace-0.3.1}/ts/src/refs/viewer/Program.tsx +0 -0
  128. {nuspace-0.3.0 → nuspace-0.3.1}/ts/src/refs/viewer/ProseRef.test.tsx +0 -0
  129. {nuspace-0.3.0 → nuspace-0.3.1}/ts/src/refs/viewer/ProseRef.tsx +0 -0
  130. {nuspace-0.3.0 → nuspace-0.3.1}/ts/src/refs/viewer/Slash.tsx +0 -0
  131. {nuspace-0.3.0 → nuspace-0.3.1}/ts/src/refs/viewer/blocks.ts +0 -0
  132. {nuspace-0.3.0 → nuspace-0.3.1}/ts/src/refs/viewer/monaco-impl.ts +0 -0
  133. {nuspace-0.3.0 → nuspace-0.3.1}/ts/src/refs/viewer/monaco.ts +0 -0
  134. {nuspace-0.3.0 → nuspace-0.3.1}/ts/src/refs/viewer/ops.ts +0 -0
  135. {nuspace-0.3.0 → nuspace-0.3.1}/ts/src/refs/viewer/prose.ts +0 -0
  136. {nuspace-0.3.0 → nuspace-0.3.1}/ts/src/refs/viewer/state.ts +0 -0
  137. {nuspace-0.3.0 → nuspace-0.3.1}/ts/src/refs/viewer/types.ts +0 -0
  138. {nuspace-0.3.0 → nuspace-0.3.1}/ts/src/refs/viewer/viewer.tsx +0 -0
  139. {nuspace-0.3.0 → nuspace-0.3.1}/ts/src/vite-env.d.ts +0 -0
  140. {nuspace-0.3.0 → nuspace-0.3.1}/ts/tsconfig.app.json +0 -0
  141. {nuspace-0.3.0 → nuspace-0.3.1}/ts/tsconfig.json +0 -0
  142. {nuspace-0.3.0 → nuspace-0.3.1}/ts/tsconfig.node.json +0 -0
  143. {nuspace-0.3.0 → nuspace-0.3.1}/ts/vite.config.ts +0 -0
  144. {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.0
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.0
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.0"
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