menlo-sdk 0.1.0rc2__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.
- menlo_sdk-0.1.0rc2/.github/workflows/ci.yml +102 -0
- menlo_sdk-0.1.0rc2/.github/workflows/publish.yml +131 -0
- menlo_sdk-0.1.0rc2/.gitignore +9 -0
- menlo_sdk-0.1.0rc2/.python-version +1 -0
- menlo_sdk-0.1.0rc2/AGENTS.md +46 -0
- menlo_sdk-0.1.0rc2/CHANGELOG.md +127 -0
- menlo_sdk-0.1.0rc2/CONTRIBUTING.md +19 -0
- menlo_sdk-0.1.0rc2/LICENSE +21 -0
- menlo_sdk-0.1.0rc2/Makefile +33 -0
- menlo_sdk-0.1.0rc2/PKG-INFO +119 -0
- menlo_sdk-0.1.0rc2/README.md +92 -0
- menlo_sdk-0.1.0rc2/RELEASING.md +104 -0
- menlo_sdk-0.1.0rc2/SECURITY.md +12 -0
- menlo_sdk-0.1.0rc2/docs/REFERENCE.md +308 -0
- menlo_sdk-0.1.0rc2/docs/SKILL.md +167 -0
- menlo_sdk-0.1.0rc2/examples/01_connect_and_read.py +27 -0
- menlo_sdk-0.1.0rc2/examples/02_stand_and_walk.py +40 -0
- menlo_sdk-0.1.0rc2/examples/03_outcomes.py +42 -0
- menlo_sdk-0.1.0rc2/examples/04_wait_and_staleness.py +39 -0
- menlo_sdk-0.1.0rc2/examples/05_stop_levels.py +36 -0
- menlo_sdk-0.1.0rc2/examples/agent_room.py +121 -0
- menlo_sdk-0.1.0rc2/examples/checkout.py +634 -0
- menlo_sdk-0.1.0rc2/examples/demos/demo_1_locomotion.py +61 -0
- menlo_sdk-0.1.0rc2/examples/demos/demo_2_telemetry.py +61 -0
- menlo_sdk-0.1.0rc2/examples/demos/demo_3_safety.py +71 -0
- menlo_sdk-0.1.0rc2/examples/follow_the_ball.py +180 -0
- menlo_sdk-0.1.0rc2/pyproject.toml +112 -0
- menlo_sdk-0.1.0rc2/src/menlo/__init__.py +15 -0
- menlo_sdk-0.1.0rc2/src/menlo/asimov/__init__.py +115 -0
- menlo_sdk-0.1.0rc2/src/menlo/asimov/_command.py +110 -0
- menlo_sdk-0.1.0rc2/src/menlo/asimov/_errors.py +115 -0
- menlo_sdk-0.1.0rc2/src/menlo/asimov/_media.py +523 -0
- menlo_sdk-0.1.0rc2/src/menlo/asimov/_outcome.py +186 -0
- menlo_sdk-0.1.0rc2/src/menlo/asimov/_proto.py +34 -0
- menlo_sdk-0.1.0rc2/src/menlo/asimov/_state.py +264 -0
- menlo_sdk-0.1.0rc2/src/menlo/asimov/connection.py +356 -0
- menlo_sdk-0.1.0rc2/src/menlo/asimov/recording.py +106 -0
- menlo_sdk-0.1.0rc2/src/menlo/asimov/robot.py +1180 -0
- menlo_sdk-0.1.0rc2/src/menlo/asimov/robots.py +55 -0
- menlo_sdk-0.1.0rc2/src/menlo/asimov/store.py +366 -0
- menlo_sdk-0.1.0rc2/src/menlo/asimov/transport/__init__.py +12 -0
- menlo_sdk-0.1.0rc2/src/menlo/asimov/transport/_livekit_client.py +552 -0
- menlo_sdk-0.1.0rc2/src/menlo/asimov/transport/_wire.py +153 -0
- menlo_sdk-0.1.0rc2/src/menlo/asimov/transport/base.py +79 -0
- menlo_sdk-0.1.0rc2/src/menlo/asimov/transport/livekit.py +319 -0
- menlo_sdk-0.1.0rc2/src/menlo/asimov/transport/udp.py +199 -0
- menlo_sdk-0.1.0rc2/src/menlo/cli.py +159 -0
- menlo_sdk-0.1.0rc2/src/menlo/py.typed +0 -0
- menlo_sdk-0.1.0rc2/tests/__init__.py +0 -0
- menlo_sdk-0.1.0rc2/tests/conftest.py +463 -0
- menlo_sdk-0.1.0rc2/tests/integration/__init__.py +0 -0
- menlo_sdk-0.1.0rc2/tests/integration/edge.pin +1 -0
- menlo_sdk-0.1.0rc2/tests/integration/test_livekit_server.py +192 -0
- menlo_sdk-0.1.0rc2/tests/integration/test_real_udp_connector.py +114 -0
- menlo_sdk-0.1.0rc2/tests/live/__init__.py +0 -0
- menlo_sdk-0.1.0rc2/tests/live/test_rig.py +52 -0
- menlo_sdk-0.1.0rc2/tests/unit/__init__.py +0 -0
- menlo_sdk-0.1.0rc2/tests/unit/test_cli.py +72 -0
- menlo_sdk-0.1.0rc2/tests/unit/test_connection.py +442 -0
- menlo_sdk-0.1.0rc2/tests/unit/test_features.py +298 -0
- menlo_sdk-0.1.0rc2/tests/unit/test_hardening.py +246 -0
- menlo_sdk-0.1.0rc2/tests/unit/test_livekit.py +418 -0
- menlo_sdk-0.1.0rc2/tests/unit/test_media.py +250 -0
- menlo_sdk-0.1.0rc2/tests/unit/test_modes.py +181 -0
- menlo_sdk-0.1.0rc2/tests/unit/test_proto_source.py +41 -0
- menlo_sdk-0.1.0rc2/tests/unit/test_robot.py +857 -0
- menlo_sdk-0.1.0rc2/tests/unit/test_store.py +244 -0
- menlo_sdk-0.1.0rc2/tests/unit/test_wire.py +125 -0
- menlo_sdk-0.1.0rc2/uv.lock +602 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
concurrency:
|
|
9
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
10
|
+
cancel-in-progress: true
|
|
11
|
+
|
|
12
|
+
permissions:
|
|
13
|
+
contents: read
|
|
14
|
+
|
|
15
|
+
jobs:
|
|
16
|
+
check:
|
|
17
|
+
name: lint + types + tests (py${{ matrix.python }})
|
|
18
|
+
runs-on: ubuntu-latest
|
|
19
|
+
strategy:
|
|
20
|
+
fail-fast: false
|
|
21
|
+
matrix:
|
|
22
|
+
python: ["3.12", "3.13"]
|
|
23
|
+
steps:
|
|
24
|
+
- uses: actions/checkout@v4
|
|
25
|
+
|
|
26
|
+
- uses: astral-sh/setup-uv@v6
|
|
27
|
+
with:
|
|
28
|
+
enable-cache: true
|
|
29
|
+
cache-dependency-glob: "uv.lock"
|
|
30
|
+
|
|
31
|
+
- name: Sync (locked)
|
|
32
|
+
run: uv sync --python ${{ matrix.python }} --locked --all-groups
|
|
33
|
+
|
|
34
|
+
- name: Ruff (lint)
|
|
35
|
+
run: uv run ruff check .
|
|
36
|
+
|
|
37
|
+
- name: Ruff (format)
|
|
38
|
+
run: uv run ruff format --check .
|
|
39
|
+
|
|
40
|
+
- name: No merge-conflict markers (they pass ruff, mypy and pytest inside a docstring)
|
|
41
|
+
run: '! git grep -nE "^(<{7}|>{7})( |$)" -- .'
|
|
42
|
+
|
|
43
|
+
- name: Mypy (strict)
|
|
44
|
+
run: uv run mypy
|
|
45
|
+
|
|
46
|
+
- name: Unit tests
|
|
47
|
+
run: uv run pytest -m "not integration and not live"
|
|
48
|
+
|
|
49
|
+
- name: Package builds and ships its bindings and type information
|
|
50
|
+
run: |
|
|
51
|
+
uv build && ls -l dist/
|
|
52
|
+
python -m zipfile -l dist/*.whl | grep -E 'menlo/py.typed'
|
|
53
|
+
unzip -p dist/*.whl '*/METADATA' | grep -E '^Requires-Dist: asimov-protocol'
|
|
54
|
+
|
|
55
|
+
# The real edge connector, in-process: proves the wire, not just the fake. Clones
|
|
56
|
+
# asimov-edge at the commit the SDK is developed against; skipped only when the
|
|
57
|
+
# token is missing, and then LOUDLY.
|
|
58
|
+
integration:
|
|
59
|
+
name: real UdpConnector (asimov-edge)
|
|
60
|
+
runs-on: ubuntu-latest
|
|
61
|
+
needs: check
|
|
62
|
+
steps:
|
|
63
|
+
- uses: actions/checkout@v4
|
|
64
|
+
- uses: astral-sh/setup-uv@v6
|
|
65
|
+
with:
|
|
66
|
+
enable-cache: true
|
|
67
|
+
cache-dependency-glob: "uv.lock"
|
|
68
|
+
# asimov-edge is an internal repo: the workflow's own token cannot read it. Without
|
|
69
|
+
# ASIMOV_REPOS_TOKEN this job warns and skips instead of failing the PR — the unit
|
|
70
|
+
# suite already drives the wire against a fake edge; this job is the extra proof.
|
|
71
|
+
- name: Token check
|
|
72
|
+
id: tok
|
|
73
|
+
env:
|
|
74
|
+
TOKEN: ${{ secrets.ASIMOV_REPOS_TOKEN }}
|
|
75
|
+
run: |
|
|
76
|
+
if [ -n "$TOKEN" ]; then
|
|
77
|
+
git config --global url."https://x-access-token:${TOKEN}@github.com/menloresearch/".insteadOf "https://github.com/menloresearch/"
|
|
78
|
+
echo "present=true" >> "$GITHUB_OUTPUT"
|
|
79
|
+
else
|
|
80
|
+
echo "::warning::ASIMOV_REPOS_TOKEN is not granted to this repository; skipping the real-edge integration job"
|
|
81
|
+
echo "present=false" >> "$GITHUB_OUTPUT"
|
|
82
|
+
fi
|
|
83
|
+
- name: Check out asimov-edge at the pinned commit
|
|
84
|
+
if: steps.tok.outputs.present == 'true'
|
|
85
|
+
run: |
|
|
86
|
+
EDGE_REF="$(sed -nE 's/^ASIMOV_EDGE_REF=(.*)$/\1/p' tests/integration/edge.pin)"
|
|
87
|
+
git clone --depth 1 https://github.com/menloresearch/asimov-edge.git /tmp/asimov-edge
|
|
88
|
+
git -C /tmp/asimov-edge fetch --depth 1 origin "$EDGE_REF"
|
|
89
|
+
git -C /tmp/asimov-edge checkout "$EDGE_REF"
|
|
90
|
+
- name: Drop the git credential before running anything from the PR
|
|
91
|
+
if: steps.tok.outputs.present == 'true'
|
|
92
|
+
run: git config --global --unset-all url."https://x-access-token:${{ secrets.ASIMOV_REPOS_TOKEN }}@github.com/menloresearch/".insteadOf
|
|
93
|
+
- name: Sync + install the edge into the same environment
|
|
94
|
+
if: steps.tok.outputs.present == 'true'
|
|
95
|
+
run: |
|
|
96
|
+
uv sync --locked --all-groups
|
|
97
|
+
uv pip install /tmp/asimov-edge
|
|
98
|
+
- name: Integration tests
|
|
99
|
+
if: steps.tok.outputs.present == 'true'
|
|
100
|
+
env:
|
|
101
|
+
ASIMOV_EDGE_SRC: /tmp/asimov-edge/src
|
|
102
|
+
run: uv run pytest -m integration
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
# Publishes the SDK on every v* tag.
|
|
4
|
+
#
|
|
5
|
+
# A build job proves the tag equals the package version
|
|
6
|
+
# and produces sdist + wheel; a separate publish job only sees the built
|
|
7
|
+
# artifacts and uploads them with PyPI trusted publishing (OIDC, no long-lived
|
|
8
|
+
# token) inside the `pypi` GitHub environment, so a required reviewer can gate
|
|
9
|
+
# it. workflow_dispatch builds without publishing (a packaging dry run).
|
|
10
|
+
#
|
|
11
|
+
# The version is dynamic (hatch reads src/menlo/__init__.py), so name and
|
|
12
|
+
# version are taken from the built wheel's METADATA, which is what PyPI sees.
|
|
13
|
+
#
|
|
14
|
+
# Setup and the tag procedure: RELEASING.md.
|
|
15
|
+
|
|
16
|
+
on:
|
|
17
|
+
push:
|
|
18
|
+
tags:
|
|
19
|
+
- 'v*'
|
|
20
|
+
workflow_dispatch:
|
|
21
|
+
|
|
22
|
+
concurrency:
|
|
23
|
+
group: publish-${{ github.ref }}
|
|
24
|
+
cancel-in-progress: false
|
|
25
|
+
|
|
26
|
+
permissions:
|
|
27
|
+
contents: read
|
|
28
|
+
|
|
29
|
+
jobs:
|
|
30
|
+
build:
|
|
31
|
+
name: Build distributions
|
|
32
|
+
runs-on: ubuntu-latest
|
|
33
|
+
outputs:
|
|
34
|
+
name: ${{ steps.meta.outputs.name }}
|
|
35
|
+
version: ${{ steps.meta.outputs.version }}
|
|
36
|
+
steps:
|
|
37
|
+
- name: Checkout
|
|
38
|
+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
39
|
+
with:
|
|
40
|
+
persist-credentials: false
|
|
41
|
+
fetch-depth: 0 # the ancestry check below needs real history, not a shallow tip
|
|
42
|
+
|
|
43
|
+
# A release is a tag on main. A tag pushed on a feature branch (or on a commit
|
|
44
|
+
# that never merged) must not publish; the tag ruleset and the pypi environment
|
|
45
|
+
# gate WHO may release, this gates WHAT. Annotated tags are peeled to the commit.
|
|
46
|
+
- name: Verify the tag is on main
|
|
47
|
+
if: startsWith(github.ref, 'refs/tags/')
|
|
48
|
+
env:
|
|
49
|
+
TAG: ${{ github.ref_name }}
|
|
50
|
+
run: |
|
|
51
|
+
# No fetch here: the checkout above (fetch-depth 0, no persisted credentials)
|
|
52
|
+
# already brought every branch, and a fetch would need a token it does not have.
|
|
53
|
+
tag_commit="$(git rev-parse "${TAG}^{commit}")"
|
|
54
|
+
if ! git merge-base --is-ancestor "$tag_commit" origin/main; then
|
|
55
|
+
echo "::error::tag $TAG ($tag_commit) is not on main; releases are tagged on main only"
|
|
56
|
+
exit 1
|
|
57
|
+
fi
|
|
58
|
+
echo "tag $TAG is on main ($tag_commit)"
|
|
59
|
+
|
|
60
|
+
- name: Set up uv
|
|
61
|
+
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
|
|
62
|
+
with:
|
|
63
|
+
enable-cache: false
|
|
64
|
+
|
|
65
|
+
- name: Build sdist and wheel
|
|
66
|
+
run: uv build --out-dir dist
|
|
67
|
+
|
|
68
|
+
- name: Read name and version from the wheel
|
|
69
|
+
id: meta
|
|
70
|
+
run: |
|
|
71
|
+
python - <<'PY' >> "$GITHUB_OUTPUT"
|
|
72
|
+
import glob, zipfile
|
|
73
|
+
from email.parser import HeaderParser
|
|
74
|
+
(whl,) = glob.glob("dist/*.whl")
|
|
75
|
+
z = zipfile.ZipFile(whl)
|
|
76
|
+
(meta,) = [n for n in z.namelist() if n.endswith(".dist-info/METADATA")]
|
|
77
|
+
h = HeaderParser().parsestr(z.read(meta).decode())
|
|
78
|
+
print(f"name={h['Name']}")
|
|
79
|
+
print(f"version={h['Version']}")
|
|
80
|
+
PY
|
|
81
|
+
cat "$GITHUB_OUTPUT"
|
|
82
|
+
|
|
83
|
+
# Tags are the releases; a tag that does not match what the wheel says
|
|
84
|
+
# must die here, not on PyPI.
|
|
85
|
+
- name: Verify tag matches package version
|
|
86
|
+
if: startsWith(github.ref, 'refs/tags/')
|
|
87
|
+
env:
|
|
88
|
+
REF: ${{ github.ref }}
|
|
89
|
+
PKG_VERSION: ${{ steps.meta.outputs.version }}
|
|
90
|
+
run: |
|
|
91
|
+
tag_version="${REF#refs/tags/v}"
|
|
92
|
+
if [ "$tag_version" != "$PKG_VERSION" ]; then
|
|
93
|
+
echo "::error::Tag version ($tag_version) does not match __version__ in src/menlo/__init__.py ($PKG_VERSION)"
|
|
94
|
+
exit 1
|
|
95
|
+
fi
|
|
96
|
+
echo "Tag and package version match: $PKG_VERSION"
|
|
97
|
+
|
|
98
|
+
# The wheel must ship the typing marker and the licence, and declare the
|
|
99
|
+
# protocol dependency — the same contract ci.yml checks.
|
|
100
|
+
- name: Check distributions
|
|
101
|
+
run: |
|
|
102
|
+
uvx twine check dist/*
|
|
103
|
+
python -m zipfile -l dist/*.whl | grep -E 'menlo/py.typed|licenses/LICENSE'
|
|
104
|
+
unzip -p dist/*.whl '*/METADATA' | grep -E '^Requires-Dist: asimov-protocol'
|
|
105
|
+
|
|
106
|
+
- name: Upload distributions
|
|
107
|
+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
|
108
|
+
with:
|
|
109
|
+
name: sdk-dist
|
|
110
|
+
path: dist/
|
|
111
|
+
retention-days: 1
|
|
112
|
+
|
|
113
|
+
publish:
|
|
114
|
+
name: Publish to PyPI
|
|
115
|
+
needs: build
|
|
116
|
+
runs-on: ubuntu-latest
|
|
117
|
+
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
|
|
118
|
+
environment:
|
|
119
|
+
name: pypi
|
|
120
|
+
url: https://pypi.org/p/${{ needs.build.outputs.name }}
|
|
121
|
+
permissions:
|
|
122
|
+
id-token: write # mandatory for trusted publishing
|
|
123
|
+
steps:
|
|
124
|
+
- name: Download distributions
|
|
125
|
+
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
|
126
|
+
with:
|
|
127
|
+
name: sdk-dist
|
|
128
|
+
path: dist/
|
|
129
|
+
|
|
130
|
+
- name: Publish package distributions to PyPI
|
|
131
|
+
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2 (twine 7: knows core metadata 2.5)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.12
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# AGENTS.md — how to work in menlo-sdk
|
|
2
|
+
|
|
3
|
+
Read this before changing anything. It is short on purpose.
|
|
4
|
+
|
|
5
|
+
## What this is
|
|
6
|
+
|
|
7
|
+
A Python SDK that drives an Asimov robot **through its edge**. It is a client of the
|
|
8
|
+
edge's arbiter and safety layer, never a bypass. The import is `menlo`, one subpackage per
|
|
9
|
+
robot: everything that speaks the Asimov wire lives in `menlo.asimov`; the top level holds
|
|
10
|
+
`__version__` and the `menlo` console script. One `Robot`; transports implement
|
|
11
|
+
`transport/base.py`. Nothing in `robot.py` may know which wire it is on.
|
|
12
|
+
|
|
13
|
+
## Rules
|
|
14
|
+
|
|
15
|
+
- **Verbs are the wire's verbs.** `set_velocity`, `stand`, `damp`, `stop`, `trajectory` —
|
|
16
|
+
the names the edge, the protocol and the robot's other controllers already use. Do not
|
|
17
|
+
invent synonyms.
|
|
18
|
+
- **No synchronous refusal.** A command returns a `Sent`; the verdict arrives as an
|
|
19
|
+
`Outcome`. `Unknown` is never success and never refusal.
|
|
20
|
+
- **State is the truth about effect.** Waits read `robot.state`, never infer from what was
|
|
21
|
+
sent. Every wait refuses to succeed on a stale stream, and a fault is reported before a
|
|
22
|
+
predicate is evaluated.
|
|
23
|
+
- **Caller bugs are builtins** (`ValueError`, `KeyError`, `RuntimeError`); robot and link
|
|
24
|
+
errors subclass `MenloError` and end in `Error`.
|
|
25
|
+
- **Nothing is guessed.** A field the robot does not report is `None`. A capability the
|
|
26
|
+
transport does not carry raises `UnsupportedError`. A joint table carries its provenance
|
|
27
|
+
(`robots.py`). A protocol version mismatch is `ProtocolMismatchError`, not a warning.
|
|
28
|
+
- **Describe what the code does.** No roadmap, no "yet", no review history in this repo.
|
|
29
|
+
Behaviour that depends on the edge or firmware is stated as the fact it is.
|
|
30
|
+
- **Tests fail without the fix.** Every behavioural change ships a test that goes red when
|
|
31
|
+
the change is reverted. The fake edge in `tests/conftest.py` speaks the real wire; if you
|
|
32
|
+
change the wire, update the fake AND run `make integration` against the real connector.
|
|
33
|
+
- **Bindings are a dependency.** `asimov-protocol>=1.2.1rc1,<2` from PyPI; `menlo.asimov._proto`
|
|
34
|
+
imports it lazily. Never copy generated `_pb2` files into this repo.
|
|
35
|
+
|
|
36
|
+
## Dependencies
|
|
37
|
+
|
|
38
|
+
Runtime core: `protobuf` only — `pip install menlo-sdk` with no extra must drive a robot
|
|
39
|
+
(the UDP lane). The media lane adds one extra, `[livekit]`, and every `livekit` import in
|
|
40
|
+
the SDK is lazy and lives in `transport/_livekit_client.py`; nothing else may import it,
|
|
41
|
+
and `robot.connect("udp")` must never reach it. Optional at call time, never at import time:
|
|
42
|
+
numpy, Pillow and OpenCV are named in an error, never depended on.
|
|
43
|
+
|
|
44
|
+
Dev: ruff, mypy (strict), pytest. Simulator for live tests:
|
|
45
|
+
`menlo-studio up --container --sdk`. A LiveKit server for `make livekit`:
|
|
46
|
+
`livekit-server --dev`.
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to menlo-sdk. Pre-1.0: minor versions may change the API.
|
|
4
|
+
Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
5
|
+
|
|
6
|
+
## 0.1.0rc2 — 2026-09-23
|
|
7
|
+
|
|
8
|
+
### Changed
|
|
9
|
+
- `asimov-protocol` is a declared dependency (`>=1.2.1rc1,<2`, from PyPI) instead of a tree
|
|
10
|
+
vendored into the wheel. One installed copy of the bindings per process; the `_vendor/`
|
|
11
|
+
directory, `scripts/vendor_protocol.sh`, `make vendor-protocol` / `check-vendor` and the
|
|
12
|
+
vendored-bindings CI job are gone.
|
|
13
|
+
- The distribution is `menlo-sdk` (repository `menloresearch/menlo-sdk`), published to PyPI on
|
|
14
|
+
`v*` tags. The import is `menlo`, one subpackage per robot: the Asimov biped is
|
|
15
|
+
`menlo.asimov` (`from menlo.asimov import Robot, Mode`). The console script is `menlo`
|
|
16
|
+
(`menlo login`), the store is `~/.menlo/robots.toml` (`$MENLO_HOME`), the environment
|
|
17
|
+
variables are `MENLO_MANAGER_URL`, `MENLO_CREDENTIAL`, `MENLO_ROBOT`, `MENLO_PERSIST`, and
|
|
18
|
+
the error base class is `MenloError`. Between releases `__version__` carries the next
|
|
19
|
+
version with a `.dev0` suffix; see `RELEASING.md`.
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
- A velocity held by `set_velocity` is released when the robot itself ends the drive: a
|
|
23
|
+
fault-DAMP (critical alert while DAMPed) drops the latch so the keepalive stops re-sending
|
|
24
|
+
it, and a firmware restart (sequence counter reset) drops it and fences off a running
|
|
25
|
+
`goto` / trajectory re-send. Nothing is sent in its place; a `MOVE` at zero would ask a
|
|
26
|
+
DAMPed or booting robot to change mode.
|
|
27
|
+
- `close()` from inside a state callback on the `livekit` lane no longer stalls 5 s on the
|
|
28
|
+
client's own event loop and then drops the queued zero-velocity packet and the room
|
|
29
|
+
leave; the client now leaves asynchronously and stops its loop once that is done.
|
|
30
|
+
- `connect(require_state=False)`: state that arrives while the transport is still opening
|
|
31
|
+
(a LiveKit media wait) now goes through the late handshake instead of being stored raw,
|
|
32
|
+
so a protocol mismatch is raised on read rather than returning a frozen sample.
|
|
33
|
+
- `connect(persist=True)`: a store that cannot be written (read-only or full `$MENLO_HOME`,
|
|
34
|
+
a name that belongs to another manager) closes the session it just opened before the
|
|
35
|
+
error propagates, instead of leaving the keepalive and the transport running.
|
|
36
|
+
- `RobotStore.put` refuses to replace an entry with one for a different manager unless the
|
|
37
|
+
caller chose the name (`menlo login --name`, `persist(name=)`): a manager answering
|
|
38
|
+
another robot's room can no longer take over that robot's saved URL and credential.
|
|
39
|
+
- A DEL byte (U+007F) in a room or robot name no longer produces a `robots.toml` that
|
|
40
|
+
`tomllib` rejects.
|
|
41
|
+
|
|
42
|
+
### Added
|
|
43
|
+
- Zero-config connect: `Robot()` with no config resolves one from `MENLO_MANAGER_URL` +
|
|
44
|
+
`MENLO_CREDENTIAL`, else from `~/.menlo/robots.toml` (`$MENLO_HOME`; `MENLO_ROBOT`
|
|
45
|
+
picks a named entry), else raises `ConnectError` naming both; `connect()` with no mode
|
|
46
|
+
takes the config's one lane. `menlo.asimov.store`: `RobotStore`, `StoredRobot`; the file is
|
|
47
|
+
0600 in a 0700 directory. `connect(persist=True)` / `MENLO_PERSIST=1` save a working URL
|
|
48
|
+
and credential after a successful connect, keyed by the serial in the robot's room.
|
|
49
|
+
- The `menlo` console script: `menlo login <manager-url> [--credential]` validates a
|
|
50
|
+
credential by minting a token exactly as `connect()` does, then saves it; `menlo robots`,
|
|
51
|
+
`menlo use <name>`, `menlo logout <name>`.
|
|
52
|
+
- `connect(require_state=False)`: the media lane without waiting for the firmware.
|
|
53
|
+
`robot.state`, `robot.info` and the motion verbs raise `NotConnectedError` until the robot
|
|
54
|
+
reports, then the handshake completes on its own; a late protocol mismatch is raised
|
|
55
|
+
where it is read.
|
|
56
|
+
- `set_velocity(..., duration=, wait=True)` blocks until the hold has ended and its zero
|
|
57
|
+
has gone out (or another verb superseded it); `close()` during a waited hold raises
|
|
58
|
+
`NotConnectedError`. `close()` documents that it cuts an unexpired hold short.
|
|
59
|
+
- `State.yaw`; `Frame.to_jpeg(quality=85) -> bytes` (Pillow, named in the `ImportError`
|
|
60
|
+
when absent; JPEG frames pass through).
|
|
61
|
+
- `ManagerConfig`: the URL needs neither scheme nor port (`192.168.22.32`, `http://host`,
|
|
62
|
+
`http://host:8080`); a loopback LiveKit URL minted by the manager (`ws://localhost:7880`,
|
|
63
|
+
the robot's own view) is rewritten to the manager's host; a session with no `label` gets
|
|
64
|
+
`<host>-<6 random hex>` so two sessions on one credential never share an identity.
|
|
65
|
+
`ManagerConfig.host`, `menlo.asimov.connection.default_label()`,
|
|
66
|
+
`ConnectionConfig.from_environment()`, `ConnectionConfig.only_mode()`;
|
|
67
|
+
`LiveKitTransport.room` / `HybridTransport.room`.
|
|
68
|
+
- `docs/SKILL.md`: the agent-facing reference for writing a script against the SDK.
|
|
69
|
+
- `ConnectionConfig(udp=UdpConfig(...), livekit=LiveKitConfig(...) | ManagerConfig(...))`
|
|
70
|
+
describes a robot's lanes, one typed class each; `Robot(cfg)` binds without touching the
|
|
71
|
+
network; `robot.connect("udp" | "hybrid" | "livekit", timeout=, media_timeout=,
|
|
72
|
+
connect_timeout=)` attaches and returns the robot; `close()` then `connect()` again switches
|
|
73
|
+
lanes on the same `Robot`. `cfg.available_modes()` says what a config can reach; a mode
|
|
74
|
+
the config cannot carry is a `ConnectError` naming the missing slot, before any I/O.
|
|
75
|
+
- `ManagerConfig(url, credential, label=)`: the SDK asks the robot's manager
|
|
76
|
+
(`POST /api/livekit/token`) for the LiveKit URL, the room and a fresh join token on every
|
|
77
|
+
connect, so a user or agent never holds a LiveKit token. `LiveKitConfig(url, room, token)`
|
|
78
|
+
is for people running their own SFU.
|
|
79
|
+
- The UDP lane: `UdpTransport` (`RobotCommand` → udp/8850, `RobotState` ← udp/8851);
|
|
80
|
+
`Robot(transport)` + `open()` for any `Transport`.
|
|
81
|
+
- Verbs `set_velocity(vx, vy, vyaw, duration=)`, `stop()`, `stand()`, `damp()`,
|
|
82
|
+
`trajectory(positions, kp=, kd=)`, `goto(positions, duration=, hz=, wait=)`; each returns a
|
|
83
|
+
`Sent` with the encoded command, the clamp flag and an outcome handle.
|
|
84
|
+
- Outcomes `Applied | Refused(reason: Refusal) | Unknown`; `Sent.wait_outcome()`,
|
|
85
|
+
`Sent.require()`. The UDP lane carries no verdicts; every outcome there is `Unknown`.
|
|
86
|
+
- Typed `State`: mode, joints by firmware name, gravity, gyro, quaternion and euler angles,
|
|
87
|
+
alerts with names and timestamps, battery (`Battery`, `BatteryProtection`), `faulted`,
|
|
88
|
+
`age_s`; `RobotInfo` with `capabilities`; `robot.has()` / `robot.require()`.
|
|
89
|
+
- Waits `wait_for(Mode)`, `wait_until(pred, timeout=, stale_after=)` with typed exits
|
|
90
|
+
`WaitTimeoutError`, `StateStaleError`, `RobotFaultedError`, `CommandRefusedError`.
|
|
91
|
+
- Media API `robot.camera`, `robot.microphone`, `robot.speaker` (`Frame`, `AudioChunk`)
|
|
92
|
+
through the `Transport` seam; `UnsupportedError` on a transport that does not carry them.
|
|
93
|
+
- Two more lanes, both first-class: `"hybrid"` (UDP control + LiveKit media) and
|
|
94
|
+
`"livekit"` (commands and
|
|
95
|
+
state as bare `RobotCommand`/`RobotState`: reliable data packets on the `commands` topic in,
|
|
96
|
+
frames of a data track named `state` out (ordered; `State.edge_timestamp_us` is the frame's
|
|
97
|
+
`user_timestamp`, the edge's receive clock) —
|
|
98
|
+
the same protobufs the UDP lane sends, no envelope, no type tag). `HybridTransport` and
|
|
99
|
+
`LiveKitTransport` underneath; `robot.py` does not know which wire it is on.
|
|
100
|
+
- LiveKit is an EXTRA (`pip install "menlo-sdk[livekit]"`): the core still depends on
|
|
101
|
+
protobuf alone, every `livekit` import is lazy inside `transport/_livekit_client.py`, and
|
|
102
|
+
`connect("udp")` never reaches it.
|
|
103
|
+
- `Camera.photo(timeout=)` returns ONE fresh `Frame`; `Camera.capture_clip(seconds,
|
|
104
|
+
audio=True)` returns a `Clip` with `save_wav()` (stdlib `wave`), `frames_as_numpy()`,
|
|
105
|
+
`save_frames()` (Pillow) and `save_mp4()` (OpenCV) — the last three raise `ImportError`
|
|
106
|
+
naming the package rather than adding a dependency. LiveKit video is converted from I420
|
|
107
|
+
to `rgb8`, so `Frame.to_numpy()` works.
|
|
108
|
+
- `Transport.silence_hint`: the transport, not `Robot`, says what to check when a connect
|
|
109
|
+
hears nothing on its wire.
|
|
110
|
+
- No `identity` parameter on the LiveKit lanes: a participant's identity is a claim inside
|
|
111
|
+
the access token and the server ignores what a client says about it, so the SDK reads it
|
|
112
|
+
back (`transport.identity`, and `endpoint` reads `room@url as <identity>`) instead of
|
|
113
|
+
accepting an argument it could not honour.
|
|
114
|
+
- Callbacks `on_state`, `on_alert`, `on_mode_change`, `on_refused`, `on_link_lost`,
|
|
115
|
+
`on_controller_change`.
|
|
116
|
+
- `robot.record(path)` JSON-lines recording and `menlo.asimov.recording.load()`.
|
|
117
|
+
- Capability honesty on the room lanes: `camera`/`microphone` are claimed only once the
|
|
118
|
+
matching track is actually subscribed, and dropped when the room goes; a room with no
|
|
119
|
+
video raises `UnsupportedError` instead of yielding nothing.
|
|
120
|
+
- Liveness: 10 Hz hold with a generation fence and bounded `duration`; `LinkLostError`
|
|
121
|
+
after `link_timeout` seconds of silence, with a zero velocity sent; `close()` zeroes a
|
|
122
|
+
held velocity; reopen with `close()` + `open()`.
|
|
123
|
+
- State-stream hygiene: samples that do not match the robot are dropped, an older datagram
|
|
124
|
+
never overwrites a newer sample (sequence compared modulo 2^32), optional `state_source`
|
|
125
|
+
allowlist.
|
|
126
|
+
- Generated `asimov.io` bindings vendored at asimov-protocol v1.1.0 with `protobuf` as the
|
|
127
|
+
only runtime dependency; an installed identical `asimov-protocol` is preferred.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
Thanks for looking. This SDK is small on purpose; changes that keep it small are welcome.
|
|
4
|
+
|
|
5
|
+
- Read `AGENTS.md` first: it is the rulebook (wire verbs, no guessing, tests that fail
|
|
6
|
+
without the fix).
|
|
7
|
+
- `make sync && make check` must pass: ruff, mypy `--strict`, and the unit suite, which
|
|
8
|
+
drives a fake edge over the real wire and needs no robot.
|
|
9
|
+
- Every behavioural change ships a test that fails on the previous code. If you change the
|
|
10
|
+
wire, update `tests/conftest.py` and run `make integration` against the real connector.
|
|
11
|
+
- Public API changes go in `CHANGELOG.md` under the unreleased version. Cutting a release
|
|
12
|
+
(version numbers, rc, tags, PyPI) is described in `RELEASING.md`.
|
|
13
|
+
- Commit messages describe the change and why; the body should let a reader reconstruct
|
|
14
|
+
the reasoning without the pull request.
|
|
15
|
+
- The `asimov.io` bindings come from the `asimov-protocol` package on PyPI
|
|
16
|
+
(`asimov-protocol>=1.2.1rc1,<2` in `pyproject.toml`). Move the floor when the SDK starts using
|
|
17
|
+
something a newer protocol MINOR added; the `<2` bound moves only with the wire directory.
|
|
18
|
+
|
|
19
|
+
Open a pull request against `main`. A review from a maintainer is required before merge.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Menlo Research
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# menlo-sdk — developer entry points. `uv` is the only tool assumed.
|
|
2
|
+
.PHONY: sync lint fmt typecheck test integration live livekit check
|
|
3
|
+
|
|
4
|
+
sync: ## create/refresh .venv from uv.lock
|
|
5
|
+
uv sync --all-groups
|
|
6
|
+
|
|
7
|
+
lint: ## ruff, the way CI runs it
|
|
8
|
+
uv run ruff check . && uv run ruff format --check .
|
|
9
|
+
|
|
10
|
+
fmt: ## fix what ruff can fix
|
|
11
|
+
uv run ruff check --fix . && uv run ruff format .
|
|
12
|
+
|
|
13
|
+
typecheck:
|
|
14
|
+
uv run mypy
|
|
15
|
+
|
|
16
|
+
test: ## unit tests only (no robot, no edge checkout, no livekit)
|
|
17
|
+
uv run pytest -m "not integration and not live and not livekit"
|
|
18
|
+
|
|
19
|
+
integration: ## the real asimov-edge UdpConnector in-process; needs ASIMOV_EDGE_SRC=<edge>/src
|
|
20
|
+
uv run pytest -m integration
|
|
21
|
+
|
|
22
|
+
live: ## a robot or `menlo-studio up --container --sdk`; needs MENLO_SDK_LIVE_HOST
|
|
23
|
+
uv run pytest -m live -s
|
|
24
|
+
|
|
25
|
+
livekit: ## real livekit.rtc vs `livekit-server --dev`. Needs MENLO_SDK_LIVEKIT_URL and
|
|
26
|
+
## TWO tokens for one room (an identity is a claim inside the JWT, so one
|
|
27
|
+
## token is one participant): MENLO_SDK_LIVEKIT_TOKEN (identity `sdk`) and
|
|
28
|
+
## MENLO_SDK_LIVEKIT_EDGE_TOKEN (identity `fake-edge`). See the test module.
|
|
29
|
+
uv run --extra livekit pytest -m livekit -s
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
check: lint typecheck test
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: menlo-sdk
|
|
3
|
+
Version: 0.1.0rc2
|
|
4
|
+
Summary: Drive an Asimov robot from Python: one Robot, pluggable transports.
|
|
5
|
+
Project-URL: Homepage, https://github.com/menloresearch/menlo-sdk
|
|
6
|
+
Project-URL: Documentation, https://github.com/menloresearch/menlo-sdk/blob/main/docs/REFERENCE.md
|
|
7
|
+
Project-URL: Repository, https://github.com/menloresearch/menlo-sdk
|
|
8
|
+
Project-URL: Issues, https://github.com/menloresearch/menlo-sdk/issues
|
|
9
|
+
Project-URL: Changelog, https://github.com/menloresearch/menlo-sdk/blob/main/CHANGELOG.md
|
|
10
|
+
Author: Menlo Research
|
|
11
|
+
License-Expression: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: asimov,humanoid,robotics,sdk
|
|
14
|
+
Classifier: Development Status :: 3 - Alpha
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Python: >=3.12
|
|
22
|
+
Requires-Dist: asimov-protocol<2,>=1.2.1rc1
|
|
23
|
+
Requires-Dist: protobuf>=5.29.3
|
|
24
|
+
Provides-Extra: livekit
|
|
25
|
+
Requires-Dist: livekit<2,>=1.1.4; extra == 'livekit'
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
<p align="center">
|
|
29
|
+
<a href="https://menlo.ai"><img src="https://docs.menlo.ai/menlo-logo.svg" alt="Menlo" width="160"></a>
|
|
30
|
+
</p>
|
|
31
|
+
|
|
32
|
+
# menlo-sdk
|
|
33
|
+
|
|
34
|
+
Drive an Asimov robot from Python. One `Robot`, five verbs, waits that read the robot's own
|
|
35
|
+
report, and camera, microphone and speaker on the same connection.
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install menlo-sdk # UDP on the LAN; Python 3.12+
|
|
39
|
+
pip install "menlo-sdk[livekit]" # + video, audio and remote connections
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Quickstart
|
|
43
|
+
|
|
44
|
+
Once per machine, save a robot. The credential is minted on the robot
|
|
45
|
+
(`asimovctl sdk-token create --role control`, or its web UI's SDK page):
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
menlo login http://asimov.local --credential <credential>
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Then a script is just:
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from menlo.asimov import Mode, Robot
|
|
55
|
+
|
|
56
|
+
with Robot().connect() as robot: # the robot you logged in to
|
|
57
|
+
if robot.state.mode is Mode.DAMP: # wake up: STAND is the only way out of DAMP
|
|
58
|
+
robot.stand()
|
|
59
|
+
robot.wait_for(Mode.STAND, timeout=15)
|
|
60
|
+
robot.set_velocity(vx=0.25, duration=4.0, wait=True) # walk 0.25 m/s for 4 s, then zero
|
|
61
|
+
robot.wait_for(Mode.MOVE) # ends in MOVE at zero velocity: standing still
|
|
62
|
+
print(robot.state.joint("L_Knee").pos, robot.state.battery)
|
|
63
|
+
print(robot.camera.photo().to_jpeg()[:4]) # needs [livekit] and Pillow
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`Robot()` finds the robot from `MENLO_MANAGER_URL` + `MENLO_CREDENTIAL`, else from
|
|
67
|
+
`~/.menlo/robots.toml` (`menlo robots`, `menlo use <name>`, `menlo logout <name>`).
|
|
68
|
+
Or say where it is: `Robot(ConnectionConfig(udp=UdpConfig("asimov.local"))).connect("udp")`.
|
|
69
|
+
|
|
70
|
+
## The verbs
|
|
71
|
+
|
|
72
|
+
| Verb | What it does |
|
|
73
|
+
|---|---|
|
|
74
|
+
| `set_velocity(vx, vy, vyaw, duration=, wait=)` | walk; held at 10 Hz until superseded, `stop()`, or `duration` |
|
|
75
|
+
| `stop()` | zero velocity; the robot keeps balancing in MOVE |
|
|
76
|
+
| `stand()` | stiffen into the standing pose; the wake-up verb, no balance loop |
|
|
77
|
+
| `damp()` | motors compliant now; a standing robot folds. The emergency verb |
|
|
78
|
+
| `goto(positions, duration=)` / `trajectory(positions)` | joint targets, radians, policy off |
|
|
79
|
+
|
|
80
|
+
Every verb returns a `Sent` at once. `wait_for(Mode.X)` and `wait_until(pred)` block on the
|
|
81
|
+
robot's report and raise `RobotFaultedError`, `StateStaleError` or `WaitTimeoutError`
|
|
82
|
+
instead of guessing. `robot.state` is the latest sample: mode, joints, IMU, alerts, battery.
|
|
83
|
+
|
|
84
|
+
## Three lanes, one API
|
|
85
|
+
|
|
86
|
+
| `connect(mode)` | control + state | video + audio | when |
|
|
87
|
+
|---|---|---|---|
|
|
88
|
+
| `"udp"` | UDP 8850 / 8851 | — | on the robot's LAN, no camera needed |
|
|
89
|
+
| `"hybrid"` | UDP | LiveKit | on the LAN, with camera |
|
|
90
|
+
| `"livekit"` | LiveKit | LiveKit | from anywhere the robot's manager is reachable |
|
|
91
|
+
|
|
92
|
+
`ManagerConfig(url, credential)` asks the robot's manager for the room and a fresh join
|
|
93
|
+
token on every connect; the SDK never holds a LiveKit secret.
|
|
94
|
+
|
|
95
|
+
## Read before you let go of the robot
|
|
96
|
+
|
|
97
|
+
- **A walk ends in MOVE at zero velocity, not in `stand()`.** STAND is a stiffen with no
|
|
98
|
+
balance loop; asking a free-standing biped to stiffen after walking tips it over.
|
|
99
|
+
- **The SDK sends zero when your script stops.** `close()`, the end of a `with` block, a
|
|
100
|
+
lost link and a fault-DAMP all release a held velocity. The edge DAMPs on its own about
|
|
101
|
+
two seconds after the last command it heard.
|
|
102
|
+
- **`damp()` folds the robot.** It is never implied by anything else.
|
|
103
|
+
- Speeds are clamped client-side (0.6 m/s, 1.5 rad/s by default) and the clamp is visible
|
|
104
|
+
on `Sent.clamped`.
|
|
105
|
+
|
|
106
|
+
## More
|
|
107
|
+
|
|
108
|
+
- [Reference](https://github.com/menloresearch/menlo-sdk/blob/main/docs/REFERENCE.md): every
|
|
109
|
+
option, the safety model in full, the wire, errors, development.
|
|
110
|
+
- [SKILL.md](https://github.com/menloresearch/menlo-sdk/blob/main/docs/SKILL.md): the
|
|
111
|
+
two-page brief for an agent writing a script against this SDK.
|
|
112
|
+
- [Examples](https://github.com/menloresearch/menlo-sdk/tree/main/examples) and the
|
|
113
|
+
[changelog](https://github.com/menloresearch/menlo-sdk/blob/main/CHANGELOG.md).
|
|
114
|
+
- Versions follow PEP 440; pre-releases (`0.1.0rc1`) install only with `pip install --pre`.
|
|
115
|
+
How releases are cut: [RELEASING.md](https://github.com/menloresearch/menlo-sdk/blob/main/RELEASING.md).
|
|
116
|
+
|
|
117
|
+
## License
|
|
118
|
+
|
|
119
|
+
MIT. The wire types come from [`asimov-protocol`](https://pypi.org/project/asimov-protocol/) (MIT).
|