orbtile-adapter 0.1.0__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.
- orbtile_adapter-0.1.0/LICENSE +21 -0
- orbtile_adapter-0.1.0/MANIFEST.in +4 -0
- orbtile_adapter-0.1.0/PKG-INFO +78 -0
- orbtile_adapter-0.1.0/README.md +61 -0
- orbtile_adapter-0.1.0/examples/tiny_agent.py +44 -0
- orbtile_adapter-0.1.0/pyproject.toml +30 -0
- orbtile_adapter-0.1.0/setup.cfg +4 -0
- orbtile_adapter-0.1.0/src/orbtile_adapter/__init__.py +663 -0
- orbtile_adapter-0.1.0/src/orbtile_adapter/py.typed +0 -0
- orbtile_adapter-0.1.0/src/orbtile_adapter.egg-info/PKG-INFO +78 -0
- orbtile_adapter-0.1.0/src/orbtile_adapter.egg-info/SOURCES.txt +12 -0
- orbtile_adapter-0.1.0/src/orbtile_adapter.egg-info/dependency_links.txt +1 -0
- orbtile_adapter-0.1.0/src/orbtile_adapter.egg-info/top_level.txt +1 -0
- orbtile_adapter-0.1.0/tests/test_sdk.py +420 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Orbtile contributors
|
|
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,78 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: orbtile-adapter
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Dependency-free sync and asyncio clients for the Orbtile Adapter API
|
|
5
|
+
Author: Orbtile contributors
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://orbtile.com/adapters
|
|
8
|
+
Project-URL: Source, https://github.com/Orbtile/orbtile-adapters
|
|
9
|
+
Project-URL: Issues, https://github.com/Orbtile/orbtile-adapters/issues
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
12
|
+
Classifier: Operating System :: MacOS
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Dynamic: license-file
|
|
17
|
+
|
|
18
|
+
# orbtile-adapter
|
|
19
|
+
|
|
20
|
+
Send your own tool's status to Orbtile: a dependency-free client for Orbtile Adapter API v1.1.
|
|
21
|
+
|
|
22
|
+
Version 0.1.0 · Python 3.10+ · stdlib runtime only · MIT. Install with `python3 -m pip install orbtile-adapter`.
|
|
23
|
+
|
|
24
|
+
## Ten-line quick start
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
from orbtile_adapter import Client, SessionUpdate
|
|
28
|
+
registration = {"name": "Tiny", "palette": {"baseHue": 150, "attentionHue": 330},
|
|
29
|
+
"capabilities": ["tap", "approvals", "questions"]}
|
|
30
|
+
with Client("dev.example.tiny", registration) as pad:
|
|
31
|
+
update: SessionUpdate = {"title": "Tiny agent", "activity": "thinking", "project": "Customer jobs"}
|
|
32
|
+
pad.upsert_session("run", **update)
|
|
33
|
+
pad.upsert_session("run", activity="streaming", message={"id": "m1", "text": "Hello"})
|
|
34
|
+
for event in pad.events():
|
|
35
|
+
pad.apply(event, lambda answer: print(answer["type"]))
|
|
36
|
+
if event["type"] == "tap":
|
|
37
|
+
break
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Discovery uses `ORBTILE_API_FILE`, or Orbtile's platform discovery path. Pass `discovery_file=` for a fake server. macOS keeps Standard then Enterprise fallback. Windows searches `%USERPROFILE%\.orbtile\adapter-api.json` (Store), `%LOCALAPPDATA%\Orbtile\Windows\adapter-api.json` (Standard MSI), then `%LOCALAPPDATA%\Orbtile Enterprise\Windows\adapter-api.json` (Enterprise MSI), choosing the first existing file and defaulting to Store. HTTP/1.1 uses the discovered Unix socket on macOS or loopback TCP address on Windows. Each connection verifies the server's HMAC proof before registration, every call sends the discovery Bearer token, and interactive routes also send the ownership token. Tokens stay in memory and are never logged.
|
|
41
|
+
|
|
42
|
+
Allow the adapter in Orbtile Settings > Agents; once allowed, it can receive approval/question decisions from the keypad. `connect()` returns the registration reply, including `enabled` and `reason`; successful session replies with `visible:false` mean user consent is pending or denied.
|
|
43
|
+
|
|
44
|
+
`SessionUpdate` and `SessionBatchItem` provide typed dictionaries, including the optional `project: str | None` field (Orbtile 0.2.17+). It sets the per-session Project group and label within its host. Orbtile cleans it and trims surrounding whitespace; the result must be 1...128 characters, otherwise the update fails with HTTP 422 `invalid_field`. Omission keeps it on merges; `project=None` clears it and restores cwd grouping. This optional field is v1-compatible; older servers ignore it.
|
|
45
|
+
|
|
46
|
+
## API
|
|
47
|
+
|
|
48
|
+
`Client(adapter_id, registration, *, discovery_file=None, heartbeat_interval=5, timeout=5)` connects through `connect()` or a context manager. `AsyncClient` has the same awaited methods and an async context manager. The asyncio facade uses stdlib worker threads; no socket operation blocks the event loop.
|
|
49
|
+
|
|
50
|
+
- `verify()`, `register()`, `heartbeat()`, `close()`; `info`, `capabilities`, `registration_reply`, `heartbeat_error` expose state. A daemon renews leases every five seconds, including while polling.
|
|
51
|
+
- `upsert_session(id, **fields)`, `remove_session(id, host="local")`, `replace_sessions([{ "id": ..., "title": ..., "activity": ... }])`. Session identity includes host; send the remote host with every update. Replacement is display-only: publish requests with the dedicated helpers.
|
|
52
|
+
- `request_approval(id, request, **fields)` and `ask_question(id, request, **fields)` set the proper activity and clear the other kind. Requests are the documented JSON objects, including fresh `id`s. Helpers do not invent an inbound decision endpoint.
|
|
53
|
+
- `poll_events(wait_seconds=2)` returns the protocol reply. `events()` is a sync/async iterator that retries network failures and overload responses with an unchanged applied cursor. Only one poll is allowed per client. `cursor` is the local applied cursor; **`lastSequence` is never automatically acknowledged**.
|
|
54
|
+
- `apply(event, commit)` validates request kind/id, session/host, local and server expiry, decision and current question options under the same lock as `answer_locally(id, commit, host="local", request_id=...)`. A duplicate/invalid event returns `False` and is discarded. An own-UI callback is ignored if the request was already consumed or its supplied `request_id` was replaced. A successful callback retires the prompt and advances the cursor; a callback that raises leaves it pending and unacknowledged. `acknowledge(event)` explicitly discards a delivered event without executing it.
|
|
55
|
+
|
|
56
|
+
Callbacks are **synchronous atomic own-UI transactions**, including for `AsyncClient` (where they run in a worker thread). They must have no side effects if they raise, or use an idempotent own-UI ledger. Check your current UI choices inside the transaction. `answer_locally` commits and retires the prompt before sending its null clear; a transport failure cannot revive that prompt. Do not run commands from event text. After `apply` succeeds, clear `approval`/`question` with a null update to retire the host UI too.
|
|
57
|
+
|
|
58
|
+
Orbtile enforces Allow-hold and Send-hold on the pad; the SDK accepts only documented final decision events after checking a live local prompt. A disconnect, timeout, denial, missing opt-in or invalid event never means Allow. Keep your own dialog available as the fallback.
|
|
59
|
+
|
|
60
|
+
On 401 the client reloads discovery once and verifies the listener again. On a lost registration it registers again, restores cached **display metadata**, resets the event cursor, and drops every old interactive request. Interactive callers receive `LeaseLostError`; `reconciliation_required` remains true even if background heartbeats subsequently succeed. Reconcile your own UI, call `reconcile()` to clear this notification, and publish new request ids. `lease_generation` changes at a lease reset. A cursor is in-memory and valid only within that live registration, never across app restarts. Transport reconnects within the registration preserve it.
|
|
61
|
+
|
|
62
|
+
Errors inherit `AdapterError`: `APIError` exposes `status`, `code`, `field`, `retry_after`; `DiscoveryError`, `ProofError`, `PayloadTooLargeError`, `LeaseLostError`, `TransportError` cover local failures. Error text never includes remote error messages or tokens. Bodies exceeding 64 KiB after UTF-8 encoding fail before connection. HTTP errors from direct methods propagate; iterators alone retry transport/overload failures.
|
|
63
|
+
|
|
64
|
+
## Tiny agent and tests
|
|
65
|
+
|
|
66
|
+
The example streams a session and publishes a question, retaining a terminal fallback when no pad answer arrives. No command execution or automatic approval occurs.
|
|
67
|
+
|
|
68
|
+
From a checkout of github.com/Orbtile/orbtile-adapters:
|
|
69
|
+
|
|
70
|
+
```sh
|
|
71
|
+
python3 -m pip install orbtile-adapter
|
|
72
|
+
python3 examples/mock-orbtile.py /tmp/orbtile-sdk-python/api.json --events
|
|
73
|
+
# Separate terminal:
|
|
74
|
+
ORBTILE_API_FILE=/tmp/orbtile-sdk-python/api.json python3 sdk/python/examples/tiny_agent.py
|
|
75
|
+
PYTHONPATH=sdk/python/src python3 -m unittest discover -s sdk/python/tests -v
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Tests use temporary Unix sockets on macOS or loopback TCP on Windows and in-process fake servers; they do not access the real app, listener or discovery directory.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# orbtile-adapter
|
|
2
|
+
|
|
3
|
+
Send your own tool's status to Orbtile: a dependency-free client for Orbtile Adapter API v1.1.
|
|
4
|
+
|
|
5
|
+
Version 0.1.0 · Python 3.10+ · stdlib runtime only · MIT. Install with `python3 -m pip install orbtile-adapter`.
|
|
6
|
+
|
|
7
|
+
## Ten-line quick start
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
from orbtile_adapter import Client, SessionUpdate
|
|
11
|
+
registration = {"name": "Tiny", "palette": {"baseHue": 150, "attentionHue": 330},
|
|
12
|
+
"capabilities": ["tap", "approvals", "questions"]}
|
|
13
|
+
with Client("dev.example.tiny", registration) as pad:
|
|
14
|
+
update: SessionUpdate = {"title": "Tiny agent", "activity": "thinking", "project": "Customer jobs"}
|
|
15
|
+
pad.upsert_session("run", **update)
|
|
16
|
+
pad.upsert_session("run", activity="streaming", message={"id": "m1", "text": "Hello"})
|
|
17
|
+
for event in pad.events():
|
|
18
|
+
pad.apply(event, lambda answer: print(answer["type"]))
|
|
19
|
+
if event["type"] == "tap":
|
|
20
|
+
break
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Discovery uses `ORBTILE_API_FILE`, or Orbtile's platform discovery path. Pass `discovery_file=` for a fake server. macOS keeps Standard then Enterprise fallback. Windows searches `%USERPROFILE%\.orbtile\adapter-api.json` (Store), `%LOCALAPPDATA%\Orbtile\Windows\adapter-api.json` (Standard MSI), then `%LOCALAPPDATA%\Orbtile Enterprise\Windows\adapter-api.json` (Enterprise MSI), choosing the first existing file and defaulting to Store. HTTP/1.1 uses the discovered Unix socket on macOS or loopback TCP address on Windows. Each connection verifies the server's HMAC proof before registration, every call sends the discovery Bearer token, and interactive routes also send the ownership token. Tokens stay in memory and are never logged.
|
|
24
|
+
|
|
25
|
+
Allow the adapter in Orbtile Settings > Agents; once allowed, it can receive approval/question decisions from the keypad. `connect()` returns the registration reply, including `enabled` and `reason`; successful session replies with `visible:false` mean user consent is pending or denied.
|
|
26
|
+
|
|
27
|
+
`SessionUpdate` and `SessionBatchItem` provide typed dictionaries, including the optional `project: str | None` field (Orbtile 0.2.17+). It sets the per-session Project group and label within its host. Orbtile cleans it and trims surrounding whitespace; the result must be 1...128 characters, otherwise the update fails with HTTP 422 `invalid_field`. Omission keeps it on merges; `project=None` clears it and restores cwd grouping. This optional field is v1-compatible; older servers ignore it.
|
|
28
|
+
|
|
29
|
+
## API
|
|
30
|
+
|
|
31
|
+
`Client(adapter_id, registration, *, discovery_file=None, heartbeat_interval=5, timeout=5)` connects through `connect()` or a context manager. `AsyncClient` has the same awaited methods and an async context manager. The asyncio facade uses stdlib worker threads; no socket operation blocks the event loop.
|
|
32
|
+
|
|
33
|
+
- `verify()`, `register()`, `heartbeat()`, `close()`; `info`, `capabilities`, `registration_reply`, `heartbeat_error` expose state. A daemon renews leases every five seconds, including while polling.
|
|
34
|
+
- `upsert_session(id, **fields)`, `remove_session(id, host="local")`, `replace_sessions([{ "id": ..., "title": ..., "activity": ... }])`. Session identity includes host; send the remote host with every update. Replacement is display-only: publish requests with the dedicated helpers.
|
|
35
|
+
- `request_approval(id, request, **fields)` and `ask_question(id, request, **fields)` set the proper activity and clear the other kind. Requests are the documented JSON objects, including fresh `id`s. Helpers do not invent an inbound decision endpoint.
|
|
36
|
+
- `poll_events(wait_seconds=2)` returns the protocol reply. `events()` is a sync/async iterator that retries network failures and overload responses with an unchanged applied cursor. Only one poll is allowed per client. `cursor` is the local applied cursor; **`lastSequence` is never automatically acknowledged**.
|
|
37
|
+
- `apply(event, commit)` validates request kind/id, session/host, local and server expiry, decision and current question options under the same lock as `answer_locally(id, commit, host="local", request_id=...)`. A duplicate/invalid event returns `False` and is discarded. An own-UI callback is ignored if the request was already consumed or its supplied `request_id` was replaced. A successful callback retires the prompt and advances the cursor; a callback that raises leaves it pending and unacknowledged. `acknowledge(event)` explicitly discards a delivered event without executing it.
|
|
38
|
+
|
|
39
|
+
Callbacks are **synchronous atomic own-UI transactions**, including for `AsyncClient` (where they run in a worker thread). They must have no side effects if they raise, or use an idempotent own-UI ledger. Check your current UI choices inside the transaction. `answer_locally` commits and retires the prompt before sending its null clear; a transport failure cannot revive that prompt. Do not run commands from event text. After `apply` succeeds, clear `approval`/`question` with a null update to retire the host UI too.
|
|
40
|
+
|
|
41
|
+
Orbtile enforces Allow-hold and Send-hold on the pad; the SDK accepts only documented final decision events after checking a live local prompt. A disconnect, timeout, denial, missing opt-in or invalid event never means Allow. Keep your own dialog available as the fallback.
|
|
42
|
+
|
|
43
|
+
On 401 the client reloads discovery once and verifies the listener again. On a lost registration it registers again, restores cached **display metadata**, resets the event cursor, and drops every old interactive request. Interactive callers receive `LeaseLostError`; `reconciliation_required` remains true even if background heartbeats subsequently succeed. Reconcile your own UI, call `reconcile()` to clear this notification, and publish new request ids. `lease_generation` changes at a lease reset. A cursor is in-memory and valid only within that live registration, never across app restarts. Transport reconnects within the registration preserve it.
|
|
44
|
+
|
|
45
|
+
Errors inherit `AdapterError`: `APIError` exposes `status`, `code`, `field`, `retry_after`; `DiscoveryError`, `ProofError`, `PayloadTooLargeError`, `LeaseLostError`, `TransportError` cover local failures. Error text never includes remote error messages or tokens. Bodies exceeding 64 KiB after UTF-8 encoding fail before connection. HTTP errors from direct methods propagate; iterators alone retry transport/overload failures.
|
|
46
|
+
|
|
47
|
+
## Tiny agent and tests
|
|
48
|
+
|
|
49
|
+
The example streams a session and publishes a question, retaining a terminal fallback when no pad answer arrives. No command execution or automatic approval occurs.
|
|
50
|
+
|
|
51
|
+
From a checkout of github.com/Orbtile/orbtile-adapters:
|
|
52
|
+
|
|
53
|
+
```sh
|
|
54
|
+
python3 -m pip install orbtile-adapter
|
|
55
|
+
python3 examples/mock-orbtile.py /tmp/orbtile-sdk-python/api.json --events
|
|
56
|
+
# Separate terminal:
|
|
57
|
+
ORBTILE_API_FILE=/tmp/orbtile-sdk-python/api.json python3 sdk/python/examples/tiny_agent.py
|
|
58
|
+
PYTHONPATH=sdk/python/src python3 -m unittest discover -s sdk/python/tests -v
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Tests use temporary Unix sockets on macOS or loopback TCP on Windows and in-process fake servers; they do not access the real app, listener or discovery directory.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Toy agent: session, streaming text, a question; own terminal remains fallback."""
|
|
3
|
+
import secrets
|
|
4
|
+
import time
|
|
5
|
+
from orbtile_adapter import Client, AdapterError
|
|
6
|
+
|
|
7
|
+
registration = {'name': 'Tiny Python', 'palette': {'baseHue': 150, 'attentionHue': 330},
|
|
8
|
+
'capabilities': ['tap', 'approvals', 'questions']}
|
|
9
|
+
with Client('dev.example.tiny.python', registration) as pad:
|
|
10
|
+
pad.upsert_session('run', title='Tiny Python agent', activity='thinking', project='Demo jobs')
|
|
11
|
+
for text in ('Hello', 'Hello from a tiny Python agent.'):
|
|
12
|
+
pad.upsert_session('run', activity='streaming', message={'id': 'm1', 'text': text})
|
|
13
|
+
time.sleep(0.2)
|
|
14
|
+
question = {'id': 'q-' + secrets.token_hex(8), 'questions': [
|
|
15
|
+
{'text': 'Which docs should I read?', 'header': 'Docs',
|
|
16
|
+
'options': [{'label': 'README'}, {'label': 'Protocol'}]}]}
|
|
17
|
+
pad.ask_question('run', question)
|
|
18
|
+
answer = []
|
|
19
|
+
def commit(event):
|
|
20
|
+
if event['type'] == 'question':
|
|
21
|
+
answer.append(event['picks'][0][0])
|
|
22
|
+
print('Answer on the pad; terminal fallback in 30 seconds.')
|
|
23
|
+
deadline = time.monotonic() + 30
|
|
24
|
+
try:
|
|
25
|
+
while not answer and time.monotonic() < deadline:
|
|
26
|
+
for event in pad.poll_events()['events']:
|
|
27
|
+
pad.apply(event, commit)
|
|
28
|
+
time.sleep(0.1) # mocks/disabled queues may return immediately
|
|
29
|
+
if answer:
|
|
30
|
+
pad.poll_events(0) # acknowledge the committed answer, never lastSequence
|
|
31
|
+
except (AdapterError, OSError):
|
|
32
|
+
print('Pad unavailable; the terminal question is still authoritative.')
|
|
33
|
+
if not answer:
|
|
34
|
+
choice = input('README [0] or Protocol [1]: ').strip()
|
|
35
|
+
if choice not in ('0', '1'):
|
|
36
|
+
raise ValueError('Choose 0 or 1; no implicit answer')
|
|
37
|
+
if pad.reconciliation_required:
|
|
38
|
+
pad.reconcile()
|
|
39
|
+
question['id'] = 'q-' + secrets.token_hex(8)
|
|
40
|
+
pad.ask_question('run', question)
|
|
41
|
+
pad.answer_locally('run', lambda: answer.append(int(choice)), request_id=question['id'])
|
|
42
|
+
pad.upsert_session('run', activity='done', question=None,
|
|
43
|
+
message={'id': 'm2', 'text': 'You chose ' + question['questions'][0]['options'][answer[0]]['label']})
|
|
44
|
+
print('Answer:', answer[0])
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "orbtile-adapter"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Dependency-free sync and asyncio clients for the Orbtile Adapter API"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{name = "Orbtile contributors"}]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
17
|
+
"Operating System :: MacOS",
|
|
18
|
+
]
|
|
19
|
+
dependencies = []
|
|
20
|
+
|
|
21
|
+
[project.urls]
|
|
22
|
+
Homepage = "https://orbtile.com/adapters"
|
|
23
|
+
Source = "https://github.com/Orbtile/orbtile-adapters"
|
|
24
|
+
Issues = "https://github.com/Orbtile/orbtile-adapters/issues"
|
|
25
|
+
|
|
26
|
+
[tool.setuptools.packages.find]
|
|
27
|
+
where = ["src"]
|
|
28
|
+
|
|
29
|
+
[tool.setuptools.package-data]
|
|
30
|
+
orbtile_adapter = ["py.typed"]
|