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.
- nu/cc/__init__.py +36 -0
- nu/cc/core.py +70 -0
- nu/cc/fabric.py +72 -0
- nu/cc/interactions.py +71 -0
- nu/cc/presets.py +49 -0
- nu/cc/refs.py +89 -0
- nu/cc/session.py +99 -0
- nu/cluster/__init__.py +45 -0
- nu/cluster/_actor.py +122 -0
- nu/cluster/interactions.py +150 -0
- nu/cluster/refs.py +112 -0
- nu/cluster/resources.py +194 -0
- nu/http/__init__.py +44 -0
- nu/http/core.py +72 -0
- nu/http/fabric.py +75 -0
- nu/http/interactions.py +246 -0
- nu/http/presets.py +51 -0
- nu/http/refs.py +352 -0
- nu/kv/__init__.py +176 -0
- nu/kv/_compat.py +44 -0
- nu/kv/fabrics/__init__.py +63 -0
- nu/kv/fabrics/codec.py +75 -0
- nu/kv/fabrics/navigator.py +68 -0
- nu/kv/fabrics/observer.py +117 -0
- nu/kv/fabrics/publisher.py +102 -0
- nu/kv/fabrics/storage.py +257 -0
- nu/kv/fabrics/transport.py +49 -0
- nu/kv/interactions/__init__.py +46 -0
- nu/kv/interactions/atomicity.py +451 -0
- nu/kv/interactions/collections.py +151 -0
- nu/kv/interactions/item.py +381 -0
- nu/kv/interactions/kh57.py +203 -0
- nu/kv/paths.py +29 -0
- nu/kv/presets.py +800 -0
- nu/kv/refs/__init__.py +98 -0
- nu/kv/refs/base.py +533 -0
- nu/kv/refs/dict.py +148 -0
- nu/kv/refs/dictshape.py +153 -0
- nu/kv/refs/items.py +300 -0
- nu/kv/refs/kh57.py +180 -0
- nu/kv/refs/kh57shape.py +178 -0
- nu/kv/refs/list.py +124 -0
- nu/kv/refs/listshape.py +126 -0
- nu/kv/refs/primitives.py +394 -0
- nu/kv/refs/prog.py +89 -0
- nu/kv/refs/set.py +89 -0
- nu/kv/refs/shape.py +129 -0
- nu/kv/refs/std.py +834 -0
- nu/kv/tree/__init__.py +8 -0
- nu/kv/tree/auto_flow_atomic.py +252 -0
- nu/kv/views/__init__.py +18 -0
- nu/kv/views/writeback.py +240 -0
- nu/llm/__init__.py +45 -0
- nu/llm/core.py +58 -0
- nu/llm/fabric.py +102 -0
- nu/llm/interactions.py +65 -0
- nu/llm/presets.py +183 -0
- nu/llm/refs.py +91 -0
- nu/mem/__init__.py +94 -0
- nu/mem/refs/__init__.py +75 -0
- nu/mem/refs/base.py +222 -0
- nu/mem/refs/dict.py +156 -0
- nu/mem/refs/dictshape.py +154 -0
- nu/mem/refs/items.py +347 -0
- nu/mem/refs/jqueue/__init__.py +35 -0
- nu/mem/refs/jqueue/form.py +111 -0
- nu/mem/refs/jqueue/interactions.py +279 -0
- nu/mem/refs/jqueue/ref.py +155 -0
- nu/mem/refs/list.py +131 -0
- nu/mem/refs/listshape.py +113 -0
- nu/mem/refs/prog.py +113 -0
- nu/mem/refs/set.py +102 -0
- nu/mem/refs/shape.py +130 -0
- nu/mem/refs/std.py +887 -0
- nu/mp/__init__.py +50 -0
- nu/mp/_worker.py +92 -0
- nu/mp/interactions.py +137 -0
- nu/mp/refs.py +83 -0
- nu/mp/resources.py +143 -0
- nu/proxy/__init__.py +49 -0
- nu/proxy/client.py +115 -0
- nu/proxy/proxy.py +137 -0
- nu/proxy/server.py +171 -0
- nu/service/__init__.py +66 -0
- nu/service/core.py +153 -0
- nu/service/fabric.py +62 -0
- nu/service/interactions.py +231 -0
- nu/service/presets.py +42 -0
- nu/service/refs.py +297 -0
- nu/std/__init__.py +52 -0
- nu/std/asyncio/__init__.py +21 -0
- nu/std/asyncio/functions.py +30 -0
- nu/std/asyncio/interactions.py +28 -0
- nu/std/cmath/__init__.py +83 -0
- nu/std/cmath/forms.py +127 -0
- nu/std/cmath/functions.py +233 -0
- nu/std/cmath/interactions.py +92 -0
- nu/std/datetime/__init__.py +18 -0
- nu/std/datetime/forms.py +789 -0
- nu/std/datetime/interactions.py +118 -0
- nu/std/decimal/__init__.py +18 -0
- nu/std/decimal/forms.py +292 -0
- nu/std/decimal/interactions.py +73 -0
- nu/std/fin/__init__.py +19 -0
- nu/std/fin/forms.py +357 -0
- nu/std/fin/interactions.py +77 -0
- nu/std/fin/native.py +275 -0
- nu/std/fractions/__init__.py +18 -0
- nu/std/fractions/forms.py +214 -0
- nu/std/fractions/interactions.py +41 -0
- nu/std/functools/__init__.py +24 -0
- nu/std/functools/functions.py +39 -0
- nu/std/functools/interactions.py +127 -0
- nu/std/itertools/__init__.py +66 -0
- nu/std/itertools/functions.py +279 -0
- nu/std/itertools/interactions.py +996 -0
- nu/std/logging/__init__.py +81 -0
- nu/std/logging/functions.py +191 -0
- nu/std/logging/interactions.py +185 -0
- nu/std/math/__init__.py +89 -0
- nu/std/math/functions.py +296 -0
- nu/std/math/interactions.py +98 -0
- nu/std/pathlib/__init__.py +19 -0
- nu/std/pathlib/forms.py +273 -0
- nu/std/pathlib/interactions.py +62 -0
- nu/std/random/__init__.py +48 -0
- nu/std/random/functions.py +136 -0
- nu/std/random/interactions.py +58 -0
- nu/std/time/__init__.py +40 -0
- nu/std/time/functions.py +108 -0
- nu/std/time/interactions.py +54 -0
- nu/std/uuid/__init__.py +18 -0
- nu/std/uuid/forms.py +187 -0
- nu/std/uuid/functions.py +56 -0
- nu/std/uuid/interactions.py +35 -0
- nu/ui/__init__.py +137 -0
- nu/ui/core/__init__.py +55 -0
- nu/ui/core/base.py +119 -0
- nu/ui/core/interactions.py +126 -0
- nu/ui/core/protocol.py +102 -0
- nu/ui/core/section.py +86 -0
- nu/ui/core/session.py +60 -0
- nu/ui/nudle/__init__.py +31 -0
- nu/ui/nudle/fabric.py +216 -0
- nu/ui/nudle/page.py +251 -0
- nu/ui/nudle/serve.py +247 -0
- nu/ui/nudle/session.py +162 -0
- nu/ui/refs/__init__.py +121 -0
- nu/ui/refs/chart.py +436 -0
- nu/ui/refs/input.py +517 -0
- nu/ui/refs/layout.py +504 -0
- nu/ui/refs/output.py +580 -0
- nu/ui/refs/structural.py +83 -0
- nustd-0.2.0.dist-info/METADATA +93 -0
- nustd-0.2.0.dist-info/RECORD +157 -0
- nustd-0.2.0.dist-info/WHEEL +4 -0
- 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
|