nustd 0.2.0__py3-none-any.whl

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 (157) hide show
  1. nu/cc/__init__.py +36 -0
  2. nu/cc/core.py +70 -0
  3. nu/cc/fabric.py +72 -0
  4. nu/cc/interactions.py +71 -0
  5. nu/cc/presets.py +49 -0
  6. nu/cc/refs.py +89 -0
  7. nu/cc/session.py +99 -0
  8. nu/cluster/__init__.py +45 -0
  9. nu/cluster/_actor.py +122 -0
  10. nu/cluster/interactions.py +150 -0
  11. nu/cluster/refs.py +112 -0
  12. nu/cluster/resources.py +194 -0
  13. nu/http/__init__.py +44 -0
  14. nu/http/core.py +72 -0
  15. nu/http/fabric.py +75 -0
  16. nu/http/interactions.py +246 -0
  17. nu/http/presets.py +51 -0
  18. nu/http/refs.py +352 -0
  19. nu/kv/__init__.py +176 -0
  20. nu/kv/_compat.py +44 -0
  21. nu/kv/fabrics/__init__.py +63 -0
  22. nu/kv/fabrics/codec.py +75 -0
  23. nu/kv/fabrics/navigator.py +68 -0
  24. nu/kv/fabrics/observer.py +117 -0
  25. nu/kv/fabrics/publisher.py +102 -0
  26. nu/kv/fabrics/storage.py +257 -0
  27. nu/kv/fabrics/transport.py +49 -0
  28. nu/kv/interactions/__init__.py +46 -0
  29. nu/kv/interactions/atomicity.py +451 -0
  30. nu/kv/interactions/collections.py +151 -0
  31. nu/kv/interactions/item.py +381 -0
  32. nu/kv/interactions/kh57.py +203 -0
  33. nu/kv/paths.py +29 -0
  34. nu/kv/presets.py +800 -0
  35. nu/kv/refs/__init__.py +98 -0
  36. nu/kv/refs/base.py +533 -0
  37. nu/kv/refs/dict.py +148 -0
  38. nu/kv/refs/dictshape.py +153 -0
  39. nu/kv/refs/items.py +300 -0
  40. nu/kv/refs/kh57.py +180 -0
  41. nu/kv/refs/kh57shape.py +178 -0
  42. nu/kv/refs/list.py +124 -0
  43. nu/kv/refs/listshape.py +126 -0
  44. nu/kv/refs/primitives.py +394 -0
  45. nu/kv/refs/prog.py +89 -0
  46. nu/kv/refs/set.py +89 -0
  47. nu/kv/refs/shape.py +129 -0
  48. nu/kv/refs/std.py +834 -0
  49. nu/kv/tree/__init__.py +8 -0
  50. nu/kv/tree/auto_flow_atomic.py +252 -0
  51. nu/kv/views/__init__.py +18 -0
  52. nu/kv/views/writeback.py +240 -0
  53. nu/llm/__init__.py +45 -0
  54. nu/llm/core.py +58 -0
  55. nu/llm/fabric.py +102 -0
  56. nu/llm/interactions.py +65 -0
  57. nu/llm/presets.py +183 -0
  58. nu/llm/refs.py +91 -0
  59. nu/mem/__init__.py +94 -0
  60. nu/mem/refs/__init__.py +75 -0
  61. nu/mem/refs/base.py +222 -0
  62. nu/mem/refs/dict.py +156 -0
  63. nu/mem/refs/dictshape.py +154 -0
  64. nu/mem/refs/items.py +347 -0
  65. nu/mem/refs/jqueue/__init__.py +35 -0
  66. nu/mem/refs/jqueue/form.py +111 -0
  67. nu/mem/refs/jqueue/interactions.py +279 -0
  68. nu/mem/refs/jqueue/ref.py +155 -0
  69. nu/mem/refs/list.py +131 -0
  70. nu/mem/refs/listshape.py +113 -0
  71. nu/mem/refs/prog.py +113 -0
  72. nu/mem/refs/set.py +102 -0
  73. nu/mem/refs/shape.py +130 -0
  74. nu/mem/refs/std.py +887 -0
  75. nu/mp/__init__.py +50 -0
  76. nu/mp/_worker.py +92 -0
  77. nu/mp/interactions.py +137 -0
  78. nu/mp/refs.py +83 -0
  79. nu/mp/resources.py +143 -0
  80. nu/proxy/__init__.py +49 -0
  81. nu/proxy/client.py +115 -0
  82. nu/proxy/proxy.py +137 -0
  83. nu/proxy/server.py +171 -0
  84. nu/service/__init__.py +66 -0
  85. nu/service/core.py +153 -0
  86. nu/service/fabric.py +62 -0
  87. nu/service/interactions.py +231 -0
  88. nu/service/presets.py +42 -0
  89. nu/service/refs.py +297 -0
  90. nu/std/__init__.py +52 -0
  91. nu/std/asyncio/__init__.py +21 -0
  92. nu/std/asyncio/functions.py +30 -0
  93. nu/std/asyncio/interactions.py +28 -0
  94. nu/std/cmath/__init__.py +83 -0
  95. nu/std/cmath/forms.py +127 -0
  96. nu/std/cmath/functions.py +233 -0
  97. nu/std/cmath/interactions.py +92 -0
  98. nu/std/datetime/__init__.py +18 -0
  99. nu/std/datetime/forms.py +789 -0
  100. nu/std/datetime/interactions.py +118 -0
  101. nu/std/decimal/__init__.py +18 -0
  102. nu/std/decimal/forms.py +292 -0
  103. nu/std/decimal/interactions.py +73 -0
  104. nu/std/fin/__init__.py +19 -0
  105. nu/std/fin/forms.py +357 -0
  106. nu/std/fin/interactions.py +77 -0
  107. nu/std/fin/native.py +275 -0
  108. nu/std/fractions/__init__.py +18 -0
  109. nu/std/fractions/forms.py +214 -0
  110. nu/std/fractions/interactions.py +41 -0
  111. nu/std/functools/__init__.py +24 -0
  112. nu/std/functools/functions.py +39 -0
  113. nu/std/functools/interactions.py +127 -0
  114. nu/std/itertools/__init__.py +66 -0
  115. nu/std/itertools/functions.py +279 -0
  116. nu/std/itertools/interactions.py +996 -0
  117. nu/std/logging/__init__.py +81 -0
  118. nu/std/logging/functions.py +191 -0
  119. nu/std/logging/interactions.py +185 -0
  120. nu/std/math/__init__.py +89 -0
  121. nu/std/math/functions.py +296 -0
  122. nu/std/math/interactions.py +98 -0
  123. nu/std/pathlib/__init__.py +19 -0
  124. nu/std/pathlib/forms.py +273 -0
  125. nu/std/pathlib/interactions.py +62 -0
  126. nu/std/random/__init__.py +48 -0
  127. nu/std/random/functions.py +136 -0
  128. nu/std/random/interactions.py +58 -0
  129. nu/std/time/__init__.py +40 -0
  130. nu/std/time/functions.py +108 -0
  131. nu/std/time/interactions.py +54 -0
  132. nu/std/uuid/__init__.py +18 -0
  133. nu/std/uuid/forms.py +187 -0
  134. nu/std/uuid/functions.py +56 -0
  135. nu/std/uuid/interactions.py +35 -0
  136. nu/ui/__init__.py +137 -0
  137. nu/ui/core/__init__.py +55 -0
  138. nu/ui/core/base.py +119 -0
  139. nu/ui/core/interactions.py +126 -0
  140. nu/ui/core/protocol.py +102 -0
  141. nu/ui/core/section.py +86 -0
  142. nu/ui/core/session.py +60 -0
  143. nu/ui/nudle/__init__.py +31 -0
  144. nu/ui/nudle/fabric.py +216 -0
  145. nu/ui/nudle/page.py +251 -0
  146. nu/ui/nudle/serve.py +247 -0
  147. nu/ui/nudle/session.py +162 -0
  148. nu/ui/refs/__init__.py +121 -0
  149. nu/ui/refs/chart.py +436 -0
  150. nu/ui/refs/input.py +517 -0
  151. nu/ui/refs/layout.py +504 -0
  152. nu/ui/refs/output.py +580 -0
  153. nu/ui/refs/structural.py +83 -0
  154. nustd-0.2.0.dist-info/METADATA +93 -0
  155. nustd-0.2.0.dist-info/RECORD +157 -0
  156. nustd-0.2.0.dist-info/WHEEL +4 -0
  157. nustd-0.2.0.dist-info/licenses/LICENSE.md +201 -0
nu/cc/__init__.py ADDED
@@ -0,0 +1,36 @@
1
+ """Nu Claude Code fabric.
2
+
3
+ Surface:
4
+ - CCFabric: holds a ClaudeAgentOptions template + runs one query per prompt call.
5
+ - PromptRef: MethodRef for a Claude Code prompt endpoint on a Service.
6
+ - CCPrompt: the interaction produced when a PromptRef is called.
7
+ - bind(service_cls, **options): Provide the CCFabric tagged by the Service class.
8
+ - Session: bracket that makes every prompt inside it continue one conversation.
9
+
10
+ Both sync and async are supported; prefer `nu.arun` for real use so cc calls
11
+ don't block the event loop (streaming, UI ticks, parallel prompts all need it).
12
+ Sync is fine for one-off scripts.
13
+
14
+ Example::
15
+
16
+ class Agent(nu.Service):
17
+ ask = nu.cc.PromptRef.method()
18
+
19
+ app = nu.With(
20
+ nu.cc.bind(Agent, model="claude-sonnet-4-5", permission_mode="acceptEdits"),
21
+ body=nu.print(nu.Dict(Agent.ask(prompt="write a haiku about rust"))["text"]),
22
+ )
23
+
24
+ asyncio.run(nu.arun(app))
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ from .fabric import CCFabric
30
+ from .interactions import CCPrompt
31
+ from .presets import bind
32
+ from .refs import PromptRef
33
+ from .session import Session, SessionHandle
34
+
35
+
36
+ __all__ = ["CCFabric", "CCPrompt", "PromptRef", "Session", "SessionHandle", "bind"]
nu/cc/core.py ADDED
@@ -0,0 +1,70 @@
1
+ """Shared compile thunks: merge defaults + call args, dispatch through CCFabric."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from typing import TYPE_CHECKING
7
+
8
+ from .fabric import CCFabric
9
+ from .session import SessionHandle
10
+
11
+
12
+ if TYPE_CHECKING:
13
+ from collections.abc import Callable
14
+
15
+ from nu.lang.runtime import Runtime
16
+
17
+
18
+ __all__ = ["acompile_call", "compile_call"]
19
+
20
+
21
+ def _split(payload: dict, args: dict) -> tuple[str, dict]:
22
+ """Pull `prompt` out; merge endpoint defaults under call overrides."""
23
+ call = dict(args)
24
+ prompt = call.pop("prompt")
25
+ merged = {**payload.get("defaults", {}), **call}
26
+ return str(prompt), merged
27
+
28
+
29
+ def _session(rt: Runtime) -> SessionHandle | None:
30
+ return rt.ctx.get(SessionHandle) if rt.ctx.has(SessionHandle) else None
31
+
32
+
33
+ def compile_call(children: tuple[Callable, ...]) -> Callable:
34
+ """Sync compile: wraps the async fabric call in asyncio.run."""
35
+ ref_thunk, args_thunk = children
36
+
37
+ def thunk(rt: Runtime) -> object:
38
+ payload = ref_thunk(rt)
39
+ args = args_thunk(rt)
40
+ prompt, overrides = _split(payload, args)
41
+ fabric = rt.ctx.get(CCFabric, payload["owner_service"])
42
+ handle = _session(rt)
43
+ if handle is not None and handle.session_id:
44
+ overrides.setdefault("resume", handle.session_id)
45
+ result = asyncio.run(fabric.aprompt(prompt, **overrides))
46
+ if handle is not None and result.get("session_id"):
47
+ handle.session_id = result["session_id"]
48
+ return result
49
+
50
+ return thunk
51
+
52
+
53
+ def acompile_call(children: tuple[Callable, ...]) -> Callable:
54
+ """Async compile: dispatch through CCFabric."""
55
+ ref_thunk, args_thunk = children
56
+
57
+ async def athunk(rt: Runtime) -> object:
58
+ payload = await ref_thunk(rt)
59
+ args = await args_thunk(rt)
60
+ prompt, overrides = _split(payload, args)
61
+ fabric = rt.ctx.get(CCFabric, payload["owner_service"])
62
+ handle = _session(rt)
63
+ if handle is not None and handle.session_id:
64
+ overrides.setdefault("resume", handle.session_id)
65
+ result = await fabric.aprompt(prompt, **overrides)
66
+ if handle is not None and result.get("session_id"):
67
+ handle.session_id = result["session_id"]
68
+ return result
69
+
70
+ return athunk
nu/cc/fabric.py ADDED
@@ -0,0 +1,72 @@
1
+ """CCFabric: config template + one async call into claude-agent-sdk.
2
+
3
+ Holds the default ClaudeAgentOptions (model, cwd, tools, system prompt, ...) for a
4
+ bound Service. Per-call overrides merge on top. `aprompt` runs one query and
5
+ collects the assistant's final text + the ResultMessage metadata.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import fields, replace
11
+ from typing import TYPE_CHECKING, Any
12
+
13
+ from claude_agent_sdk import (
14
+ AssistantMessage,
15
+ ClaudeAgentOptions,
16
+ ResultMessage,
17
+ TextBlock,
18
+ query,
19
+ )
20
+
21
+
22
+ if TYPE_CHECKING:
23
+ from nu.lang.runtime import Context
24
+
25
+
26
+ __all__ = ["CCFabric"]
27
+
28
+
29
+ class CCFabric:
30
+ """Holds a ClaudeAgentOptions template. One query per prompt call."""
31
+
32
+ def __init__(self, *, options: ClaudeAgentOptions | None = None, **defaults: object) -> None:
33
+ self.options = options or ClaudeAgentOptions(**defaults) # type: ignore[arg-type]
34
+
35
+ def setup(self, ctx: Context) -> None: # noqa: D102
36
+ pass
37
+
38
+ def cleanup(self) -> None: # noqa: D102
39
+ pass
40
+
41
+ async def asetup(self, ctx: Context) -> None: # noqa: D102
42
+ pass
43
+
44
+ async def acleanup(self) -> None: # noqa: D102
45
+ pass
46
+
47
+ def _merge(self, overrides: dict[str, object]) -> ClaudeAgentOptions:
48
+ if not overrides:
49
+ return self.options
50
+ allowed = {f.name for f in fields(self.options)}
51
+ clean = {k: v for k, v in overrides.items() if k in allowed and v is not None}
52
+ return replace(self.options, **clean) if clean else self.options
53
+
54
+ async def aprompt(self, prompt: str, **overrides: object) -> dict[str, Any]:
55
+ """Run one prompt turn. Returns {text, session_id, total_cost_usd, duration_ms, num_turns}."""
56
+ opts = self._merge(overrides)
57
+ text_parts: list[str] = []
58
+ meta: dict[str, Any] = {}
59
+ async for msg in query(prompt=prompt, options=opts):
60
+ if isinstance(msg, AssistantMessage):
61
+ for block in msg.content:
62
+ if isinstance(block, TextBlock):
63
+ text_parts.append(block.text)
64
+ elif isinstance(msg, ResultMessage):
65
+ meta = {
66
+ "session_id": getattr(msg, "session_id", None),
67
+ "total_cost_usd": getattr(msg, "total_cost_usd", None),
68
+ "duration_ms": getattr(msg, "duration_ms", None),
69
+ "num_turns": getattr(msg, "num_turns", None),
70
+ "result": getattr(msg, "result", None),
71
+ }
72
+ return {"text": meta.get("result") or "".join(text_parts), **meta}
nu/cc/interactions.py ADDED
@@ -0,0 +1,71 @@
1
+ """CCPrompt: ScalarAction that runs one Claude Code prompt turn."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ from nu.engine.structure import Declared
8
+ from nu.lang import ScalarAction
9
+
10
+ from .core import acompile_call, compile_call
11
+
12
+
13
+ if TYPE_CHECKING:
14
+ from collections.abc import Callable
15
+
16
+
17
+ __all__ = ["CCPrompt"]
18
+
19
+
20
+ class CCPrompt(ScalarAction):
21
+ """One prompt turn against the Claude Code agent a PromptRef addresses.
22
+
23
+ Built by calling a PromptRef rather than written by hand. At evaluation
24
+ it resolves the Ref, merges the endpoint's declared defaults under this
25
+ call's overrides, and drives one ``query`` through the ``CCFabric``
26
+ provided for the owning Service, draining the message stream to the end.
27
+
28
+ A whole agent run happens inside this one node: the agent may read
29
+ files, run tools and take many turns before the stream closes. Only the
30
+ final text and the run's accounting come back out.
31
+
32
+ Args:
33
+ ref: the PromptRef naming the agent.
34
+ args: a Dict carrying ``prompt`` plus this call's option overrides.
35
+
36
+ Notes:
37
+ - Declared as mutating its Ref child, so runs against one agent stay
38
+ ordered and are never folded together.
39
+ - Under a ``nu.cc.Session`` bracket it reads the session id off the
40
+ handle and resumes; the first call in the bracket starts fresh and
41
+ writes its id back for the rest.
42
+ - An explicit ``resume=`` override wins over the bracket's handle.
43
+ - The sync path drives the async SDK through ``asyncio.run``, so it
44
+ raises if a loop is already running. Use ``nu.arun`` anywhere near
45
+ an event loop.
46
+
47
+ Yields:
48
+ A dict with ``text`` plus the run's accounting: ``session_id``,
49
+ ``total_cost_usd``, ``duration_ms``, ``num_turns`` and the raw
50
+ ``result``. ``text`` is the SDK's final result string, falling back
51
+ to the concatenated assistant text blocks. If the stream ends
52
+ without a result message the accounting keys are absent entirely,
53
+ not None.
54
+
55
+ Example:
56
+ class Agent(nu.Service):
57
+ ask = nu.cc.PromptRef.method()
58
+ app = nu.With(
59
+ nu.cc.bind(Agent, model="claude-sonnet-4-5", permission_mode="acceptEdits"),
60
+ body=nu.print(nu.dict(Agent.ask("write a haiku about rust"))["text"]),
61
+ )
62
+ asyncio.run(nu.arun(app))
63
+ """
64
+
65
+ _mutates = Declared(value=frozenset({0}), name="mutates")
66
+
67
+ def _compile(self, nid: int, children: tuple[Callable, ...]) -> Callable:
68
+ return compile_call(children)
69
+
70
+ def _acompile(self, nid: int, children: tuple[Callable, ...]) -> Callable:
71
+ return acompile_call(children)
nu/cc/presets.py ADDED
@@ -0,0 +1,49 @@
1
+ """bind(): Provide a CCFabric for a Service."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from nu.context.fabric import Provide
6
+
7
+ from .fabric import CCFabric
8
+
9
+
10
+ __all__ = ["bind"]
11
+
12
+
13
+ def bind(service_cls: type, **defaults: object) -> Provide:
14
+ """Configure the Claude Code agent a Service's PromptRefs run against.
15
+
16
+ What it provides is tagged by the Service class, which is how a PromptRef
17
+ declared on that class finds its fabric, and how one program can run
18
+ several agents with different tools, directories or system prompts side
19
+ by side.
20
+
21
+ Args:
22
+ service_cls: the Service whose PromptRefs this agent serves.
23
+ **defaults: ``ClaudeAgentOptions`` fields - ``model``, ``cwd``,
24
+ ``allowed_tools``, ``system_prompt``, ``permission_mode``,
25
+ ``max_turns``, and the rest. Pass ``options=`` with a built
26
+ ``ClaudeAgentOptions`` instead to bypass the kwargs entirely.
27
+
28
+ Notes:
29
+ - The kwargs are ``ClaudeAgentOptions`` fields - ``model``, ``cwd``,
30
+ ``allowed_tools``, ``system_prompt``, ``permission_mode``,
31
+ ``max_turns`` and the rest - or a single ``options=`` holding a
32
+ built ``ClaudeAgentOptions``.
33
+ - These are the outermost layer: declaration defaults sit on top of
34
+ them and per-call overrides on top of those.
35
+ - Unlike the LLM fabric there is no client to open, so the
36
+ ``With`` block costs nothing until a prompt actually runs.
37
+ - Everything here reaches the ``ClaudeAgentOptions`` constructor as
38
+ written, so a bad key raises when the block is entered - unlike a
39
+ per-call override, which is dropped silently.
40
+ - ``options=`` and loose kwargs do not combine: given both, the
41
+ built options object is used and the kwargs are ignored.
42
+
43
+ Yields:
44
+ A Provide to hand to ``nu.With``.
45
+
46
+ Example:
47
+ app = nu.With(nu.cc.bind(Agent, model="claude-sonnet-4-5", cwd="/srv/repo"), body=...)
48
+ """
49
+ return Provide(CCFabric, defaults, tag=service_cls)
nu/cc/refs.py ADDED
@@ -0,0 +1,89 @@
1
+ """PromptRef: Ref addressing a Claude Code prompt endpoint on a Service.
2
+
3
+ Mirrors nu.http verb refs: `.method(**defaults)` returns a Method declaration
4
+ that the ServiceMeta descriptor unwraps at class access; calling the Ref with
5
+ kwargs produces a CCPrompt interaction.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import TYPE_CHECKING
11
+
12
+ from nu.domains.service import Method, MethodRef
13
+ from nu.forms import Dict
14
+
15
+ from .interactions import CCPrompt
16
+
17
+
18
+ if TYPE_CHECKING:
19
+ from nu.lang import Nu
20
+
21
+
22
+ __all__ = ["PromptRef"]
23
+
24
+
25
+ class PromptRef(MethodRef):
26
+ """Addresses a Claude Code agent on a Service, one Ref per agent role.
27
+
28
+ Written in a Service class body. The Ref carries no configuration of its
29
+ own beyond its declared defaults: the model, working directory, tool
30
+ allowlist and system prompt come from the ``CCFabric`` provided for the
31
+ owning Service class, so a program can hold several Services each
32
+ standing for a differently-configured agent.
33
+
34
+ Notes:
35
+ - Every call spawns a fresh Claude Code session unless it runs
36
+ inside a ``nu.cc.Session`` bracket, which threads the session id
37
+ through so the calls read as one conversation.
38
+ - The Ref needs the ``claude-agent-sdk`` package and a working
39
+ ``claude`` CLI on the machine that evaluates it.
40
+
41
+ Example:
42
+ class Agent(nu.Service):
43
+ ask = nu.cc.PromptRef.method(max_turns=1)
44
+ app = nu.With(
45
+ nu.cc.bind(Agent, model="claude-sonnet-4-5", cwd="/tmp"),
46
+ body=nu.print(nu.dict(Agent.ask("name this directory"))["text"]),
47
+ )
48
+ asyncio.run(nu.arun(app))
49
+ """
50
+
51
+ @classmethod
52
+ def method(cls, **defaults: object) -> PromptRef: # type: ignore[override]
53
+ """Declare a prompt endpoint whose defaults every call through it inherits.
54
+
55
+ Args:
56
+ **defaults: ``ClaudeAgentOptions`` fields to apply on top of the
57
+ bind for calls through this endpoint (``model``,
58
+ ``max_turns``, ``allowed_tools``, ``permission_mode``, ...).
59
+ A per-call kwarg of the same name wins.
60
+
61
+ Notes:
62
+ - Annotated as returning the Ref, but at run time it returns a
63
+ ``Method`` declaration that the ServiceMeta descriptor unwraps
64
+ at class access. The lie makes ``Agent.ask`` type-check as a
65
+ PromptRef.
66
+ - Keys that are not ``ClaudeAgentOptions`` fields are dropped
67
+ silently when the call runs, so a misspelt option is not an
68
+ error, it is a no-op.
69
+ """
70
+ return Method(cls, defaults=defaults) # type: ignore[return-value]
71
+
72
+ def __call__(self, prompt: object, **overrides: object) -> Nu:
73
+ """Build a CCPrompt interaction over one turn's prompt and options.
74
+
75
+ Args:
76
+ prompt: the text to send. Stringified at evaluation, so it may
77
+ be a Nu term rather than a literal.
78
+ **overrides: ``ClaudeAgentOptions`` fields for this call only.
79
+
80
+ Notes:
81
+ - Keywords beyond the prompt are ``ClaudeAgentOptions`` fields
82
+ applied to this call only.
83
+ - Unlike ChatRef, the prompt is positional and required: there
84
+ is no messages-list form, since the transcript is the
85
+ session's business rather than the caller's.
86
+ - Prompt and overrides land together in one ``Dict`` child, so
87
+ both are resolved at evaluation.
88
+ """
89
+ return CCPrompt(self, Dict.of(prompt=prompt, **overrides))
nu/cc/session.py ADDED
@@ -0,0 +1,99 @@
1
+ """Session: lifecycle bracket that scopes a cc session across nested prompts.
2
+
3
+ Mirrors the nu.kv pattern (Snapshot / Transaction): a lazy handle is bound into
4
+ the ctx on entry; every PromptRef call inside the bracket reads it and threads
5
+ `resume=session_id` so cc treats the calls as one continuous session.
6
+
7
+ The first prompt starts a fresh cc session (no resume); its returned session_id
8
+ is captured on the handle, and subsequent prompts continue it.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from contextlib import contextmanager
14
+ from typing import TYPE_CHECKING
15
+
16
+ from nu.flows.strategy import Sequential
17
+ from nu.spans.bracket import _LifecycleBracket
18
+
19
+
20
+ if TYPE_CHECKING:
21
+ from collections.abc import Iterator
22
+
23
+ from nu.lang import Nu
24
+ from nu.lang.runtime import Context
25
+
26
+
27
+ __all__ = ["Session", "SessionHandle"]
28
+
29
+
30
+ class SessionHandle:
31
+ """The mutable cell a Session binds, holding the id once a prompt has produced one.
32
+
33
+ Empty until the first prompt inside the bracket returns; from then on
34
+ every prompt in the bracket reads the id off it and resumes. Bound in
35
+ the Context under its own type, which is how the compiled prompt thunks
36
+ find it without the Session being their parent.
37
+ """
38
+
39
+ __slots__ = ("session_id",)
40
+
41
+ def __init__(self) -> None:
42
+ self.session_id: str | None = None
43
+
44
+
45
+ def _wrap_body(children: tuple[Nu, ...]) -> Nu:
46
+ if len(children) == 1:
47
+ return children[0]
48
+ return Sequential(*children)
49
+
50
+
51
+ class Session(_LifecycleBracket):
52
+ """Makes every prompt in its body continue one Claude Code conversation.
53
+
54
+ Without it each prompt is a cold start that remembers nothing. The
55
+ bracket binds a fresh handle on entry; the first prompt underneath runs
56
+ without resuming and writes the id cc gave it onto the handle, and each
57
+ later prompt reads it back and resumes, so the agent keeps its context
58
+ across the whole body.
59
+
60
+ Args:
61
+ *body: the terms to run inside the session. Several are run in
62
+ order, as if wrapped in ``Sequential``.
63
+
64
+ Notes:
65
+ - Reach is by Context, not by ownership: any prompt evaluated
66
+ while the bracket is open joins the session, including ones
67
+ inside functions the body calls.
68
+ - A nested Session binds its own handle and shadows the outer one,
69
+ so its prompts form a separate conversation. Sibling Sessions
70
+ likewise never share.
71
+ - The handle is bound at the same point whether the run is sync or
72
+ async, so the bracket behaves the same under ``nu.run`` and
73
+ ``nu.arun``.
74
+ - Nothing is persisted. The id lives for as long as the bracket is
75
+ open; to pick a conversation back up later, keep the
76
+ ``session_id`` a prompt yielded and pass it as ``resume=``.
77
+
78
+ Yields:
79
+ Whatever the body yields; the bracket adds nothing of its own.
80
+
81
+ Example:
82
+ class Agent(nu.Service):
83
+ ask = nu.cc.PromptRef.method()
84
+ app = nu.With(
85
+ nu.cc.bind(Agent, model="claude-sonnet-4-5"),
86
+ body=nu.cc.Session(
87
+ nu.print(nu.dict(Agent.ask("pick a number between 1 and 10"))["text"]),
88
+ nu.print(nu.dict(Agent.ask("what number did you pick?"))["text"]),
89
+ ),
90
+ )
91
+ asyncio.run(nu.arun(app))
92
+ """
93
+
94
+ def __init__(self, *body: Nu) -> None:
95
+ super().__init__(_wrap_body(body))
96
+
97
+ @contextmanager
98
+ def _open(self, ctx: Context) -> Iterator[Context]:
99
+ yield ctx.bind(SessionHandle, SessionHandle())
nu/cluster/__init__.py ADDED
@@ -0,0 +1,45 @@
1
+ """nu.cluster - the ray compute fabric.
2
+
3
+ Ray reframes as a compute fabric: locations are actor processes, addresses
4
+ are tags, the interaction is ``Teleport`` (execute a Nu tree there).
5
+
6
+ - ``RayCluster`` - the cluster handle FabricLifecycle. On asetup ensures
7
+ ray is initialized; on acleanup shuts down its own init (if any).
8
+ - ``RayService`` - one remote actor hosting a Nu ``Context`` + tree
9
+ executor. Provisioned per-instance by ``Provide`` / ``ProvideList`` /
10
+ ``ProvideDict``.
11
+ - ``RayClusterRef`` / ``RayServiceRef`` - fabric refs. ``RayServiceRef``
12
+ takes an arbitrary hashable tag (``RayServiceRef("ledger-main")``,
13
+ ``RayServiceRef(("ledger", 0))``).
14
+ - ``Teleport`` - the interaction; ships the body term to a tagged
15
+ ``RayService`` and awaits its result.
16
+
17
+ Typical shape::
18
+
19
+ Provide(RayCluster, {"address": "auto"},
20
+ ProvideList(RayService, [
21
+ {"actor_name": "worker-0", "num_cpus": 4},
22
+ {"actor_name": "worker-1", "num_cpus": 4},
23
+ ],
24
+ Sequential(
25
+ Teleport(some_tree, target=0),
26
+ Teleport(some_tree, target=1),
27
+ ),
28
+ ),
29
+ )
30
+ """
31
+
32
+ from __future__ import annotations
33
+
34
+ from .interactions import Teleport
35
+ from .refs import RayClusterRef, RayServiceRef
36
+ from .resources import RayCluster, RayService
37
+
38
+
39
+ __all__ = [
40
+ "RayCluster",
41
+ "RayClusterRef",
42
+ "RayService",
43
+ "RayServiceRef",
44
+ "Teleport",
45
+ ]
nu/cluster/_actor.py ADDED
@@ -0,0 +1,122 @@
1
+ """``_RayServiceActor``: a ``@ray.remote`` host process for Nu execution.
2
+
3
+ The actor holds a Nu ``Context`` and executes Nu trees against it. It is the
4
+ in-actor half of a ``RayService`` - the ``RayService`` resource on the parent
5
+ side spawns one of these actors, tells it to build its context, and later
6
+ routes tree execution to it through ``aexecute``.
7
+
8
+ An in-flight ``aexecute`` is tracked so shutdown can cancel and drain it
9
+ before the actor tears down its context. This avoids a use-after-free when
10
+ closing storage under a live query.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import asyncio
16
+ import contextlib
17
+ from typing import TYPE_CHECKING
18
+
19
+ import ray
20
+
21
+
22
+ if TYPE_CHECKING:
23
+ from collections.abc import Awaitable, Callable
24
+
25
+ from nu.lang.runtime import Context
26
+ from nu.spans.bracket import _LifecycleBracket
27
+
28
+
29
+ @ray.remote
30
+ class _RayServiceActor:
31
+ """Ray actor hosting a Nu ``Context`` + tree executor.
32
+
33
+ Parent side (``RayService``) calls the async ``start`` / ``aexecute`` /
34
+ ``shutdown`` methods. The actor itself is stateless before ``start`` and
35
+ torn down after ``shutdown``.
36
+ """
37
+
38
+ def __init__(self) -> None:
39
+ self._ctx: Context | None = None
40
+ self._inflight: set[asyncio.Task] = set()
41
+ self._init_stack: contextlib.AsyncExitStack | None = None
42
+
43
+ async def start(
44
+ self,
45
+ init: _LifecycleBracket | None,
46
+ ctx_builder: Callable[[], Context | Awaitable[Context]] | None,
47
+ ) -> None:
48
+ """Build this actor's Context.
49
+
50
+ Prefers ``init`` (a lifecycle bracket): enters its ``_aopen`` on a
51
+ fresh ``Context()``, saves the resulting Context, and keeps the exit
52
+ stack open so the bracket's resources stay live until ``shutdown``.
53
+
54
+ Falls back to ``ctx_builder`` (a callable returning a Context or
55
+ awaitable). If both are ``None``, the actor gets a bare Context.
56
+ """
57
+ from nu.lang.runtime import Context
58
+
59
+ if init is not None:
60
+ stack = contextlib.AsyncExitStack()
61
+ await stack.__aenter__()
62
+ try:
63
+ self._ctx = await stack.enter_async_context(init._aopen(Context()))
64
+ except BaseException:
65
+ await stack.__aexit__(None, None, None)
66
+ raise
67
+ self._init_stack = stack
68
+ return
69
+ if ctx_builder is None:
70
+ self._ctx = Context()
71
+ return
72
+ result = ctx_builder()
73
+ if asyncio.iscoroutine(result):
74
+ result = await result
75
+ self._ctx = result
76
+
77
+ async def aexecute(self, tree: object, attrs: dict | None = None) -> object:
78
+ """Compile ``tree`` and evaluate it against this actor's Context.
79
+
80
+ Returns the root's value (``None`` for effect-only trees). ``attrs``
81
+ is merged into a shallow-copied Context before execution so the
82
+ parent's ``ctx.attrs`` can carry over without polluting the actor's
83
+ baseline.
84
+
85
+ Value-rooted trees (Query / Command / effectful Sequential) work
86
+ directly. A stream-rooted tree returns its async generator, which
87
+ won't cross the ray boundary; wrap it in a reducer or ``last()``
88
+ before ``Teleport``.
89
+ """
90
+ from nu.lang.helpers import aeval
91
+ from nu.lang.helpers import compile as compile_term
92
+
93
+ ctx = self._ctx
94
+ if attrs:
95
+ ctx = ctx._copy()
96
+ for key, value in attrs.items():
97
+ ctx.attrs[key] = value
98
+
99
+ task = asyncio.current_task()
100
+ if task is not None:
101
+ self._inflight.add(task)
102
+ try:
103
+ program = compile_term(tree)
104
+ value, _ = await aeval(program, ctx)
105
+ return value
106
+ finally:
107
+ if task is not None:
108
+ self._inflight.discard(task)
109
+
110
+ async def shutdown(self) -> None:
111
+ """Cancel in-flight executes, tear down init bracket, drop the Context."""
112
+ for t in list(self._inflight):
113
+ t.cancel()
114
+ for t in list(self._inflight):
115
+ with contextlib.suppress(asyncio.CancelledError, Exception):
116
+ await t
117
+ self._inflight.clear()
118
+ if self._init_stack is not None:
119
+ with contextlib.suppress(Exception):
120
+ await self._init_stack.__aexit__(None, None, None)
121
+ self._init_stack = None
122
+ self._ctx = None