Metadata-Version: 2.1
Name: dynos-client
Version: 0.1.3
Summary: Client and CLI for orchestrating DYNOS backends remotely
License: Apache-2.0
Requires-Python: >=3.8
Description-Content-Type: text/markdown
Requires-Dist: dynos-core >=0.1.3
Requires-Dist: requests
Requires-Dist: typer
Requires-Dist: bcrypt >=4.0
Requires-Dist: rich

# dynos-client

The HTTP client and command-line tool for talking to a DYNOS planning backend.
You install this on your laptop or server, point it at a backend, and drive the
planner from Python or the shell. The backend does the planning; this package
gives you the network interface.

You usually combine it with a domain package (e.g. `dynos-sentry-domain`)
that gives you the actual nouns and verbs your goals reference. `dynos-client`
ships only the infrastructure: HTTP, CLI, mission builder, session-gateway
authentication.

`dynos-client` depends on `dynos-core`, as do the domain files. The domain
files also depend on this package. Specific robot missions or extensions,
such as the adaptive sampling demo, depend on robot implementations.

Most users start here. `pip install dynos-client` plus a domain package and you
can talk to a hosted backend immediately. You do not need to install the
backend itself.

## Install

```bash
pip install dynos-client
```

Pulls in `dynos-core`, `requests`, `typer`, and `bcrypt`. Registers a `dynos` console script.

## Sessions and the backend

DYNOS backends run in two modes. Direct mode is a single process with no auth.
This is fine for local development against a mock, or on-vehicle work.
Session-gateway mode is multi-tenant, and most likely how you're be developing:
a TLS endpoint accepts password logins, issues bearer tokens, and routes each
user's traffic to a private subprocess (their session) with its own world
model. The hosted backend at `https://api.dynosplan.com` is in this mode.

You authenticate once:

```bash
dynos login
# optionally, you can set the source. The default is the webpage, but this might be an on-ship server:
# dynos login --to https://api.dynosplan.com
```

That writes `~/.dynos/config.json` (mode 0o600). Every subsequent `dynos call`,
`dynos session`, and `dynos connect` reads the cached URL, token, and default
session, so the `--to` flag is rarely needed after the first login.

A session is a private process that holds *your* world model. Create one before
issuing goals:

```bash
dynos session create
dynos call health         # confirms the gateway is reachable
dynos session info        # confirms a session is selected
```

`dynos session swap <name>` switches the default; `dynos session stop` ends the
current session. The CLI never silently picks a session for you. If your
default session has expired or been stopped, commands error out with a hint to
run `swap` or `create`.

## CLI map

| Command                             | Purpose                                                                                                                |
|-------------------------------------|------------------------------------------------------------------------------------------------------------------------|
| `dynos login` / `logout` / `whoami` | Authenticate against a session gateway and inspect the cache.                                                          |
| `dynos call <subcmd>`               | Talk to a running backend: `health`, `goal`, `plan`, `execute`, `state`, `objects`, `trigger`, `reset`, `geojson`, ... |
| `dynos session <subcmd>`            | Manage gateway sessions: `create`, `list`, `info`, `swap`, `extend`, `reset`, `stop`.                                  |
| `dynos connect <module>:<Class>`    | Run as a *servant* — the backend dispatches actions over HTTP and your local code executes them.                       |
| `dynos hash-password`               | Produce a bcrypt hash for the gateway's admin user table. You can send this to the server admin (me) to change your pw |

Run any of them with `--help` for full options.

## 60-second example

```python
from dynos_client import RemoteOrchestrator

orch = RemoteOrchestrator.from_config()    # reads ~/.dynos/config.json
print(orch.health())
```

The shell equivalent:

```bash
dynos call health
```

Both go to the same endpoint. The Python form is the one you reach for when
missions get longer than the CLI's 30-second HTTP timeout (see "Long-running
plans" below).

## Goal vs. trigger

There are two ways to make the backend do something, and they are not
interchangeable.

`dynos call execute` (preferred). You set a goal, which is a desired symbolic
state. The planner derives the prerequisite sequence. If something fails, the
planner replans from the current state. This is what the system is designed
for.

```bash
dynos call goal "full_coverage_of(site_alpha)" "phase_ascending()"
dynos call plan        # optional: print the action sequence the planner picked
dynos call execute
```

`dynos call trigger` (debug only). Fires one transition directly, with no
planner involvement and no precondition check. Convenient for confirming a
custom `@Action` is wired up; not safe for anything that moves the vehicle. A
careless `trigger` can leave the symbolic world in a state the planner doesn't
trust (e.g. recording `at(wp_end)` without descent ever happening).

```bash
dynos call trigger "resample_zone(source_zone=site_alpha)"   # OK for debug
```

If you find yourself wanting to chain `trigger` calls, you actually want a
goal. Let the planner sequence them.

## Missions

A `Mission` chains plan blocks and action blocks. Each block runs to completion
before the next starts; the planner replans at every block boundary using the
state after the previous block.

```python
from dynos_client import RemoteOrchestrator, Mission
from dynos_sentry.sentry import Zone, full_coverage_of

orch = RemoteOrchestrator.from_config(timeout_s=3600)

area_north = Zone("area_north", coordinate_frame="geographic",
    vertices=[[-70.67, 41.525], [-70.66, 41.525], [-70.66, 41.53], [-70.67, 41.53]],
    altitude=70.0, speed=0.8, coverage_width=170.0, robot_width=0.5)
area_south = Zone("area_south", coordinate_frame="geographic",
    vertices=[[-70.67, 41.52], [-70.66, 41.52], [-70.66, 41.525], [-70.67, 41.525]],
    altitude=70.0, speed=0.8, coverage_width=170.0, robot_width=0.5)
orch.create_object(area_north)
orch.create_object(area_south)

mission = Mission(name="two_surveys")
mission.add_plan([full_coverage_of(area_north)])
mission.add_plan([full_coverage_of(area_south)])
results = orch.execute_blocks(mission)
```

Mix action blocks (run a single transition immediately, no planning) with plan
blocks (set a goal, planner derives the sequence):

```python
from dynos_sentry.sentry import full_coverage_of

mission = Mission(name="custom_sequence")
mission.add_action(takeover_control)                # primitive, no planning
mission.add_plan([full_coverage_of(area_north)])    # planned
mission.add_plan([full_coverage_of(area_south)])
mission.add_plan([phase_ascending()])
results = orch.execute_blocks(mission)
```

A single plan block can carry multiple goals; the planner finds one plan that
achieves all of them. A single action block instructs a specific thing to
occur, which circumvents a lot of the replanning possibilities (you strip out
the context), but sometimes things need to be hard-coded in a mission so it's
available. Prefer @Script for hard sequences, because these can be associated
with transitions like actions do (see later).

## Servants and `dynos connect`

When the action's implementation lives on your machine — your ML model, a
sensor handler, a custom controller — you run a *servant*. The backend keeps
planning; your code executes the assignments it dispatches. See
`dynos-adaptive-resampling`'s README for a full worked example.

```bash
dynos connect my_package.runnable:MyNode
```

A Runnable is a small class with three static methods (`register`,
`get_start_state`, `get_start_goal`); the servant loop polls the backend for
assignments and posts results back. `dynos connect` reads the same cached login
as `dynos call`.

## Custom actions in a client-only env

`@Action` and `@Script` (the decorators that bind a Python method to a
`Transition`) live in the DYNOS backend, not in `dynos-client`. If your code
needs to import them but should also run in environments where the backend
isn't installed (CI, tests, a public release), use the conditional-import
recipe:

```python
try:
    from dynos.databases.typed_action import Action, Script
except ImportError:
    def Action(*a, **kw):
        return (lambda f: f) if not (a and callable(a[0])) else a[0]
    Script = Action
```

This degrades the decorators to no-ops in client-only environments; the class
still loads, registration is a no-op, and the action never fires (because no
backend is there to dispatch it).

## Long-running plans

`dynos call execute` is synchronous: the backend holds the HTTP response open
until every step in the plan has finished. The CLI defaults to a 30-second
client-side timeout, so for any plan that takes longer your terminal will print
`Read timed out` even though the backend is still happily executing. This times
out the client, not the plan.

Two workarounds:

1. Use Python with a longer timeout. `RemoteOrchestrator.from_config(timeout_s=3600)`.
2. Don't watch, open a second terminal and poll. `dynos call state`, `dynos call goal`, `dynos call trace`, `dynos call log --wall`.

## Configuration

`~/.dynos/config.json` (mode 0o600) stores:

| Key               | Source                                |
|-------------------|---------------------------------------|
| `backend_url`     | `dynos login --to ...`                |
| `token`           | `dynos login` (gateway-issued bearer) |
| `username`        | `dynos login`                         |
| `role`            | `dynos login` (`user`, `admin`)       |
| `default_session` | `dynos session create` / `swap`       |

Set `DYNOS_CONFIG_PATH=/some/other/file.json` to override the location (handy for tests; the public CLI honours the env var).

## Public API

| Symbol                           | Purpose                                                              |
|----------------------------------|----------------------------------------------------------------------|
| `RemoteOrchestrator`             | The HTTP client; mirrors the backend's `Orchestrator` Python API.    |
| `RemoteAuthExpired`              | Raised when the cached token is rejected; catch and reauthenticate.  |
| `Mission`                        | Builder for compound goal sequences.                                 |
| `PlanBlock`                      | Plan-derived block within a `Mission` (one or more goals → planner). |
| `ActionBlock` / `AnyActionBlock` | Single-action block within a `Mission` (no planning).                |

## Next

Install `dynos-sentry-domain` to get `Zone`, `Coordinate`, `full_coverage_of`,
and the rest of the goal vocabulary. The walkthrough at `user_guide.md` ties
everything together end-to-end. A simple toy domain (warehouse) is also
available.
