rpc and node backbone
Project description
rekuest-next
Self-documenting, asynchronous, scalable RPC for untrusted actors — accessible through FastAPI or deployed on the Arkitekt platform.
Turn an ordinary, typed Python function into a remotely callable action: rekuest inspects its signature and docstring to build a self-documenting schema, hosts it as a provisionable actor, and lets other apps discover and call it — with fine-grained, per-app access control.
Why rekuest?
Most RPC frameworks expose functionality on a server. rekuest flips that around: functionality lives on the client, and any app can provide actions on a user's behalf. That makes it a good fit for scientific and automation workflows where:
- Code is the contract. Type hints and docstrings become the schema — args, returns, widgets, and documentation are derived automatically, no IDL to maintain.
- Apps are untrusted. Every app negotiates access to data and to other apps via OAuth2, so you can safely run third-party or user-contributed code.
- Work is distributed. Many agents can provide the same action; the platform load balances, retries, and fails over calls across them.
- Calls compose. An action can call other actions, so you can build pipelines that span apps and machines.
How it works
@register function ──inspect──▶ Action (self-documenting schema)
│ │
│ hosted by an Agent │ discovered + reserved by callers
▼ ▼
Actor ◀───────── assign / call ─────┘
(running instance,
expands inputs, runs,
shrinks outputs)
| Term | Meaning |
|---|---|
| Action | The documented concept of a function that is available on the platform. |
| Implementation | A concrete realization of an Action provided by an App. |
| App | A provider of functionality that negotiates access rights through OAuth2. |
| Agent | A running instance of an App; the host of actors. Agents connect and disconnect. |
| Actor | A provisioned, running instance of a function that processes assignments. |
| Provision | A contract obliging an Agent to keep an action available (think Deployment in K8s). |
| Reservation | A contract for a user to call one or more actors; the platform load balances and heals it. |
Prerequisites
A running rekuest server (most easily obtained through an Arkitekt deployment).
Install
rekuest-next is usually consumed through the Arkitekt platform, which wires up the
server connection, authentication and lifecycle for you. Installing arkitekt-next
pulls in rekuest-next as well:
pip install arkitekt-next
Doing image analysis? The Arkitekt platform ships ready-made data structures for imaging (see mikro).
Providing an action
Register a typed function — its signature and docstring become the schema:
from arkitekt_next import register, easy
@register
def add_greeting(x: int, name: str) -> str:
"""Add a greeting.
Takes a number and a name and returns a friendly message.
Args:
x (int): A number to include in the message.
name (str): The name to greet.
Returns:
str: The greeting message.
"""
return f"Hello {name}, your number is {x}"
with easy("my_app") as app:
# Connects, registers the action, and provides it until interrupted.
app.run()
Run it during development with:
arkitekt-next run dev
The action is now registered under your app and signed-in user, and can be provisioned and called by other apps. By default users may only assign to their own apps; this can be relaxed on the rekuest server.
Calling an action
Resolve an action with find (by its id, or pass an Action straight through), then
invoke it with call (sync) or acall (async):
from arkitekt_next import easy
from rekuest_next import find, call
with easy("my_app") as app:
action = find(action_id) # action_id discovered via search / the platform
result = call(action, x=1, name="world")
print(result) # "Hello world, your number is 1"
When you already know which agent provides an implementation, you can address it directly and await the result:
from rekuest_next.api.schema import amy_implementation_at
from rekuest_next.remote import acall
impl = await amy_implementation_at(instance_id, "add_greeting")
result = await acall(impl, x=1, name="world")
Working with complex data structures
rekuest serializes and documents the standard Python types out of the box:
str · bool · int · float · Enum · dict · list
Large or complex objects (e.g. numpy arrays) should not be serialized into messages. rekuest offers two strategies instead.
Global structures — reference-by-id
Store the object in central storage and pass only a reference. Make a class a
structure with the @structure decorator — give it an identifier and async
ashrink/aexpand methods:
from rekuest_next import structure
@structure(identifier="myapp/image")
class Image:
id: str # a reference to this object in central storage
async def ashrink(self) -> str:
return self.id
@classmethod
async def aexpand(cls, value: str) -> "Image":
return await cls.load_from_server(value)
The decorator registers the structure eagerly. A class that instead implements a
get_identifier()classmethod alongsideashrink/aexpandis still picked up automatically the first time it is used in a type hint.
Now you can use Image directly in type hints — rekuest automatically ashrinks
(serializes) it to its reference when sending and aexpands (deserializes) it back on
the receiving side:
@register
def brightest_pixel(image: Image) -> int:
return image.max()
Memory structures — keep it on the shelve
Sometimes an object only makes sense within the agent that produced it and never needs to leave the machine — a handle to an open file, a live model, an intermediate result. Any plain class with no serialization protocol is treated as a memory structure: instead of being serialized, it is parked on the agent's in-memory shelve, and only an opaque drawer reference travels on the wire.
class Session: # no get_identifier / ashrink / aexpand -> memory structure
def __init__(self, handle):
self.handle = handle
@register
def open_session(path: str) -> Session:
return Session(open(path))
@register
def read_session(session: Session) -> str:
return session.handle.read()
Because the object stays on the agent's shelve, piping the reference returned by
open_session into read_session resolves the same live object — no copy, no
re-serialization. See tests/test_app_memory_structure.py for end-to-end examples.
State
Actions are stateless by default — each call is independent. When an agent needs to
remember something across calls (a connection, a counter, a loaded model), declare an
observable state with @state on a dataclass:
from dataclasses import dataclass
from rekuest_next import state, startup
@state
@dataclass
class CounterState:
count: int = 0
@startup
def initialize() -> CounterState:
# Startup hooks return the initial state instances for the agent.
return CounterState(count=0)
Any action can then read and mutate that state simply by type-hinting a parameter with the state class — rekuest injects the live instance:
@register
def increment(counter: CounterState) -> int:
counter.count += 1 # mutation is published to the platform automatically
return counter.count
State is shared by all of an agent's actors and is observable: changes are published
live (at publish_interval), so dashboards and other apps can watch it in real time. Use
@state(local_only=True) to keep a state on the agent without exposing it to the platform.
Dependencies
An action can call out to functionality provided by another agent. You describe what
you need with a dependency protocol — @declare (a.k.a. agent_protocol) inspects the
class so that its public methods become action demands and its @declare_state
attributes become state demands:
from rekuest_next import declare, declare_state
@declare_state
class CameraState:
connected: bool
@declare(app="lab")
class Camera:
state: CameraState
async def snap(self, exposure_ms: float) -> bytes:
... # body is only a signature — it is fulfilled by a remote agent
Consume the dependency by type-hinting a parameter with the protocol class. rekuest
resolves a matching remote agent and injects a proxy you can call — .call(...)
(sync) or .acall(...) (async). Calls are routed through your agent and tracked as
children of the current assignment:
@register
def capture(camera: Camera) -> bytes:
# Calls the remote agent's `snap` action and returns its result.
return camera.snap(exposure_ms=10.0)
The platform load balances across matching agents and will heal the reservation if one
disconnects. Use auto_resolvable=True, min=, and max= on @declare to control how
many matching agents may be bound automatically.
Bloks — dashboards from JSX
A blok is a declarative UI panel an app contributes to the platform — a small
dashboard built from a component tree, wired straight to your actions and state. You
write it as a JSX/XML string; jsx(...) parses it into a component tree (with helpful
line/column errors when it can't):
from rekuest_next import jsx
panel = jsx(
"""
<Page>
<Label text="Camera" />
<Text value="$state.camera.connected" />
<Button title="Snap" onClick="@camera.snap(exposure_ms=10)" />
</Page>
"""
)
app.register_blok(name="camera_panel", component=panel, description="Camera dashboard")
Props follow three conventions, which is what ties a blok to the rest of your app:
| Prop value | Meaning |
|---|---|
text="Camera" |
A static value. |
value="$state.camera.connected" |
A dynamic binding to a dependency's state ($state.<dependency>.<path>). |
onClick="@camera.snap(exposure_ms=10)" |
An action callback that calls a dependency's action (@<dependency>.<operation>(...)). Use @utils.<op>(...) for built-in utilities. |
Bloks derive their action and state dependencies automatically from the paths they reference, so the platform knows exactly what each panel needs to run.
Development
uv sync # install dependencies
pytest -m "not integration" # fast unit tests
pytest -m integration # full integration tests (require Docker)
Integration tests spin up the rekuest stack with Docker Compose and exercise real
calls against it; see tests/conftest.py.
Learn more
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file rekuest_next-2.0.0rc3.tar.gz.
File metadata
- Download URL: rekuest_next-2.0.0rc3.tar.gz
- Upload date:
- Size: 225.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.24 {"installer":{"name":"uv","version":"0.11.24","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
bc5c93888a5807cdea52b5d6e97d77581b3613bb9c2b90a80079c8fd3e95a881
|
|
| MD5 |
15239963a3ace42599dbaaa083a5bc32
|
|
| BLAKE2b-256 |
5cfb7cf8808618ce74c316b4b4c7b7b8ee92c765b052653604d3a94ceac489c0
|
File details
Details for the file rekuest_next-2.0.0rc3-py3-none-any.whl.
File metadata
- Download URL: rekuest_next-2.0.0rc3-py3-none-any.whl
- Upload date:
- Size: 279.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.24 {"installer":{"name":"uv","version":"0.11.24","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
09d68ab2cf276482238f0cc01d0c9e82cccabb387bb575e0fee655f0dfe57762
|
|
| MD5 |
ac7358145533f5076f0fe0ae0e6c8c93
|
|
| BLAKE2b-256 |
430ff3a799b9006583b69e466738d9381774a19aeb92e2b98e7ff0b2b0d63a53
|