awp-python 0.1.0a4__tar.gz → 0.1.0a6__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.
- awp_python-0.1.0a6/AGENTS.md +58 -0
- {awp_python-0.1.0a4 → awp_python-0.1.0a6}/CHANGELOG.md +16 -8
- awp_python-0.1.0a6/CLAUDE.md +1 -0
- {awp_python-0.1.0a4 → awp_python-0.1.0a6}/PKG-INFO +21 -21
- {awp_python-0.1.0a4 → awp_python-0.1.0a6}/README.md +20 -20
- {awp_python-0.1.0a4 → awp_python-0.1.0a6}/conformance/README.md +11 -11
- {awp_python-0.1.0a4 → awp_python-0.1.0a6}/conformance/core-agent-lockstep.json +31 -7
- {awp_python-0.1.0a4 → awp_python-0.1.0a6}/conformance/core-agent-streaming.json +29 -5
- {awp_python-0.1.0a4 → awp_python-0.1.0a6}/pyproject.toml +1 -1
- {awp_python-0.1.0a4 → awp_python-0.1.0a6}/src/awp/__init__.py +1 -1
- {awp_python-0.1.0a4 → awp_python-0.1.0a6}/src/awp/aio.py +24 -27
- {awp_python-0.1.0a4 → awp_python-0.1.0a6}/src/awp/client.py +89 -32
- {awp_python-0.1.0a4 → awp_python-0.1.0a6}/src/awp/schema.py +7 -9
- {awp_python-0.1.0a4 → awp_python-0.1.0a6}/tests/test_aio.py +12 -0
- awp_python-0.1.0a6/tests/test_client.py +100 -0
- {awp_python-0.1.0a4 → awp_python-0.1.0a6}/tests/test_client_traces.py +1 -3
- {awp_python-0.1.0a4 → awp_python-0.1.0a6}/tests/test_schema.py +12 -9
- {awp_python-0.1.0a4 → awp_python-0.1.0a6}/tests/world.py +2 -2
- {awp_python-0.1.0a4 → awp_python-0.1.0a6}/uv.lock +1 -1
- awp_python-0.1.0a4/tests/test_client.py +0 -31
- {awp_python-0.1.0a4 → awp_python-0.1.0a6}/.gitignore +0 -0
- {awp_python-0.1.0a4 → awp_python-0.1.0a6}/.gitmodules +0 -0
- {awp_python-0.1.0a4 → awp_python-0.1.0a6}/.python-version +0 -0
- {awp_python-0.1.0a4 → awp_python-0.1.0a6}/LICENSE +0 -0
- {awp_python-0.1.0a4 → awp_python-0.1.0a6}/conformance/frames.json +0 -0
- {awp_python-0.1.0a4 → awp_python-0.1.0a6}/conformance/manifest-lockstep.json +0 -0
- {awp_python-0.1.0a4 → awp_python-0.1.0a6}/conformance/manifest-streaming.json +0 -0
- {awp_python-0.1.0a4 → awp_python-0.1.0a6}/scripts/sync_spec.py +0 -0
- {awp_python-0.1.0a4 → awp_python-0.1.0a6}/src/awp/_spec/lifecycle.json +0 -0
- {awp_python-0.1.0a4 → awp_python-0.1.0a6}/src/awp/_spec/schemas/action-cancel-result.schema.json +0 -0
- {awp_python-0.1.0a4 → awp_python-0.1.0a6}/src/awp/_spec/schemas/action-ref.schema.json +0 -0
- {awp_python-0.1.0a4 → awp_python-0.1.0a6}/src/awp/_spec/schemas/action-schema.schema.json +0 -0
- {awp_python-0.1.0a4 → awp_python-0.1.0a6}/src/awp/_spec/schemas/action-status.schema.json +0 -0
- {awp_python-0.1.0a4 → awp_python-0.1.0a6}/src/awp/_spec/schemas/action-submit-result.schema.json +0 -0
- {awp_python-0.1.0a4 → awp_python-0.1.0a6}/src/awp/_spec/schemas/action-submit.schema.json +0 -0
- {awp_python-0.1.0a4 → awp_python-0.1.0a6}/src/awp/_spec/schemas/agent-manifest.schema.json +0 -0
- {awp_python-0.1.0a4 → awp_python-0.1.0a6}/src/awp/_spec/schemas/approval-requested.schema.json +0 -0
- {awp_python-0.1.0a4 → awp_python-0.1.0a6}/src/awp/_spec/schemas/approval-respond.schema.json +0 -0
- {awp_python-0.1.0a4 → awp_python-0.1.0a6}/src/awp/_spec/schemas/common.schema.json +0 -0
- {awp_python-0.1.0a4 → awp_python-0.1.0a6}/src/awp/_spec/schemas/embodiment.schema.json +0 -0
- {awp_python-0.1.0a4 → awp_python-0.1.0a6}/src/awp/_spec/schemas/empty-result.schema.json +0 -0
- {awp_python-0.1.0a4 → awp_python-0.1.0a6}/src/awp/_spec/schemas/error.schema.json +0 -0
- {awp_python-0.1.0a4 → awp_python-0.1.0a6}/src/awp/_spec/schemas/frame-inline.schema.json +0 -0
- {awp_python-0.1.0a4 → awp_python-0.1.0a6}/src/awp/_spec/schemas/frame-tree.schema.json +0 -0
- {awp_python-0.1.0a4 → awp_python-0.1.0a6}/src/awp/_spec/schemas/obs-report.schema.json +0 -0
- {awp_python-0.1.0a4 → awp_python-0.1.0a6}/src/awp/_spec/schemas/observation-channel.schema.json +0 -0
- {awp_python-0.1.0a4 → awp_python-0.1.0a6}/src/awp/_spec/schemas/ping-result.schema.json +0 -0
- {awp_python-0.1.0a4 → awp_python-0.1.0a6}/src/awp/_spec/schemas/ping.schema.json +0 -0
- {awp_python-0.1.0a4 → awp_python-0.1.0a6}/src/awp/_spec/schemas/profiles/gui-actions.schema.json +0 -0
- {awp_python-0.1.0a4 → awp_python-0.1.0a6}/src/awp/_spec/schemas/reset-result.schema.json +0 -0
- {awp_python-0.1.0a4 → awp_python-0.1.0a6}/src/awp/_spec/schemas/reset.schema.json +0 -0
- {awp_python-0.1.0a4 → awp_python-0.1.0a6}/src/awp/_spec/schemas/restore.schema.json +0 -0
- {awp_python-0.1.0a4 → awp_python-0.1.0a6}/src/awp/_spec/schemas/safety-policy.schema.json +0 -0
- {awp_python-0.1.0a4 → awp_python-0.1.0a6}/src/awp/_spec/schemas/session-open.schema.json +0 -0
- {awp_python-0.1.0a4 → awp_python-0.1.0a6}/src/awp/_spec/schemas/session-ready.schema.json +0 -0
- {awp_python-0.1.0a4 → awp_python-0.1.0a6}/src/awp/_spec/schemas/session-resume.schema.json +0 -0
- {awp_python-0.1.0a4 → awp_python-0.1.0a6}/src/awp/_spec/schemas/session-state.schema.json +0 -0
- {awp_python-0.1.0a4 → awp_python-0.1.0a6}/src/awp/_spec/schemas/session-telemetry.schema.json +0 -0
- {awp_python-0.1.0a4 → awp_python-0.1.0a6}/src/awp/_spec/schemas/session-transfer-result.schema.json +0 -0
- {awp_python-0.1.0a4 → awp_python-0.1.0a6}/src/awp/_spec/schemas/session-transfer.schema.json +0 -0
- {awp_python-0.1.0a4 → awp_python-0.1.0a6}/src/awp/_spec/schemas/snapshot-result.schema.json +0 -0
- {awp_python-0.1.0a4 → awp_python-0.1.0a6}/src/awp/_spec/schemas/subscribe-result.schema.json +0 -0
- {awp_python-0.1.0a4 → awp_python-0.1.0a6}/src/awp/_spec/schemas/subscribe.schema.json +0 -0
- {awp_python-0.1.0a4 → awp_python-0.1.0a6}/src/awp/_spec/schemas/task-update.schema.json +0 -0
- {awp_python-0.1.0a4 → awp_python-0.1.0a6}/src/awp/_spec/schemas/tick-result.schema.json +0 -0
- {awp_python-0.1.0a4 → awp_python-0.1.0a6}/src/awp/_spec/schemas/tick.schema.json +0 -0
- {awp_python-0.1.0a4 → awp_python-0.1.0a6}/src/awp/_spec/schemas/unsubscribe.schema.json +0 -0
- {awp_python-0.1.0a4 → awp_python-0.1.0a6}/src/awp/_spec/schemas/world-event.schema.json +0 -0
- {awp_python-0.1.0a4 → awp_python-0.1.0a6}/src/awp/_spec/schemas/world-manifest.schema.json +0 -0
- {awp_python-0.1.0a4 → awp_python-0.1.0a6}/src/awp/clock.py +0 -0
- {awp_python-0.1.0a4 → awp_python-0.1.0a6}/src/awp/demo.py +0 -0
- {awp_python-0.1.0a4 → awp_python-0.1.0a6}/src/awp/errors.py +0 -0
- {awp_python-0.1.0a4 → awp_python-0.1.0a6}/src/awp/frames.py +0 -0
- {awp_python-0.1.0a4 → awp_python-0.1.0a6}/src/awp/jsonrpc.py +0 -0
- {awp_python-0.1.0a4 → awp_python-0.1.0a6}/src/awp/lifecycle.py +0 -0
- {awp_python-0.1.0a4 → awp_python-0.1.0a6}/src/awp/py.typed +0 -0
- {awp_python-0.1.0a4 → awp_python-0.1.0a6}/tests/__init__.py +0 -0
- {awp_python-0.1.0a4 → awp_python-0.1.0a6}/tests/conftest.py +0 -0
- {awp_python-0.1.0a4 → awp_python-0.1.0a6}/tests/test_evidence.py +0 -0
- {awp_python-0.1.0a4 → awp_python-0.1.0a6}/tests/test_frames.py +0 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
awp-python is the Python agent SDK for the Agent World Protocol, published on PyPI as `awp-python`. `awp.ClientConnection` is the protocol as a sans-IO state machine, and `awp.aio.AsyncClient` drives it over a WebSocket. It targets one specification revision, which is pinned as the `spec/` submodule and named by `awp.SPEC_REVISION`.
|
|
4
|
+
|
|
5
|
+
## Checks
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
git submodule update --init
|
|
9
|
+
uv sync
|
|
10
|
+
uv run ruff check && uv run ruff format --check
|
|
11
|
+
uv run mypy
|
|
12
|
+
uv run pytest --cov
|
|
13
|
+
uv run python scripts/sync_spec.py --check
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
CI runs all of these on Python 3.11–3.13. It also runs awp-conformance against `awp-demo` in both time models, and fails unless the claim is AWP-conformant.
|
|
17
|
+
|
|
18
|
+
## Code
|
|
19
|
+
|
|
20
|
+
- Protocol behavior belongs in `ClientConnection`. `AsyncClient` adds only transport, timers, and awaiting.
|
|
21
|
+
- Cite a requirement ID (`AWP-XXX-NNN`) where the code implements it. Otherwise, comment only what the code can't say.
|
|
22
|
+
- Tests run against the scripted world in `tests/world.py`, not against awp-sim.
|
|
23
|
+
- `src/awp/_spec/` is copied from `spec/` by `scripts/sync_spec.py`. Never edit it by hand.
|
|
24
|
+
|
|
25
|
+
## Conformance
|
|
26
|
+
|
|
27
|
+
`conformance/` holds the reports behind the README's claim, plus the evidence for their `manual` rows. The reports must come from the suite version CI runs. After bumping that pin in `.github/workflows/ci.yml`, regenerate them with the commands in `conformance/README.md`. Then update the versions named there and in the README.
|
|
28
|
+
|
|
29
|
+
## Commits and pull requests
|
|
30
|
+
|
|
31
|
+
- Branch from `main` and open a pull request. Merge once CI passes.
|
|
32
|
+
- Write the title as one plain sentence in sentence case, with no trailing period, saying what changed: `Run awp-conformance 0.1.0a4 in CI`. When the change is part of a release, end it with the version: `(0.1.0a4)`.
|
|
33
|
+
- Add a body only when the title can't carry the reason: one or two short sentences.
|
|
34
|
+
- Write commits the way a person on the project would. No `Co-Authored-By` trailers, no "Generated with" lines, and no other mention of AI tools, in commits or in PRs.
|
|
35
|
+
- The PR title matches the commit title, and the description is a few lines at most.
|
|
36
|
+
|
|
37
|
+
## Moving to a new draft revision
|
|
38
|
+
|
|
39
|
+
1. Check out the revision's tag (`spec-v0.1-draft.N`) in `spec/`.
|
|
40
|
+
2. Run `uv run python scripts/sync_spec.py`.
|
|
41
|
+
3. Update `SPEC_REVISION` in `src/awp/__init__.py`.
|
|
42
|
+
4. Fix whatever the tests report.
|
|
43
|
+
|
|
44
|
+
## Releasing
|
|
45
|
+
|
|
46
|
+
1. In a pull request:
|
|
47
|
+
- Set the version with `uv version <version>`, for example `0.1.0a5`.
|
|
48
|
+
- Add a `CHANGELOG.md` entry that starts "Targets specification revision `0.1-draft.N`." and lists the user-visible changes.
|
|
49
|
+
2. Once it is merged, tag the merge commit and push the tag:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
git tag -a v0.1.0a5 -m "awp-python 0.1.0a5 (AWP 0.1-draft.N)"
|
|
53
|
+
git push origin v0.1.0a5
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
3. `release.yml` checks that the tag matches the version, then builds the package. It publishes through PyPI trusted publishing once someone approves the `pypi` environment. A maintainer gives that approval. Agents never approve deployments, and never publish with a token.
|
|
57
|
+
|
|
58
|
+
awp-sim depends on awp-python, so release awp-python first when both change.
|
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.0a6
|
|
4
|
+
|
|
5
|
+
Targets specification revision `0.1-draft.10`.
|
|
6
|
+
|
|
7
|
+
- When one of a multi-bind session's embodiments is transferred away, the client stops counting it as bound (AWP-EMB-003).
|
|
8
|
+
|
|
9
|
+
## 0.1.0a5
|
|
10
|
+
|
|
11
|
+
Targets specification revision `0.1-draft.9`.
|
|
12
|
+
|
|
13
|
+
- `open_session` takes `embodiments` to bind several embodiments of one `multi_bind_group`, and `submit` then requires `embodiment_id` (AWP-EMB-005).
|
|
14
|
+
- A response that arrives after `AsyncClient.call` times out is still processed, so its `status_seq` and tick are not lost. `call`, `wait_for`, and `advance` accept `timeout=None`.
|
|
15
|
+
- `advance()` refuses while a `world.tick` is pending: under a `barrier` it may wait for other sessions (AWP-TIM-012). In lockstep the heartbeat timer runs even with `heartbeat=False`.
|
|
16
|
+
- Closing a session stops its heartbeat acknowledgements, stream connection, and resumption. A new session starts from none of the last one's channel, sequencing, or frame state, and `AsyncClient.latest` is cleared.
|
|
17
|
+
- `ClientConnection` gains `lockstep`, `heartbeat_s`, and `embodiments`. `forget` is gone.
|
|
18
|
+
|
|
3
19
|
## 0.1.0a4
|
|
4
20
|
|
|
5
21
|
Targets specification revision `0.1-draft.9`.
|
|
@@ -33,13 +49,5 @@ First release, targeting specification revision `0.1-draft.7`.
|
|
|
33
49
|
- The `ws` stream binding: `awp-sim serve --stream-binding ws` offers frames on a stream connection in the binary envelope, and `AsyncClient` uses it when offered, re-establishing it if only the stream drops (AWP-TRN-010..013).
|
|
34
50
|
- Beyond Core, each enabled with `awp-sim serve --features`: `task`, `approval` (the `park` action; approvers connect with `--approver-token`), `blend`, `transfer`, `sim` (seeding, snapshot and restore, replay bundles with `--replay-dir`, and `awp-sim replay`), and `servo` (a command channel). `ClientConnection` gains `update_task`, `transfer`, `reset`, `snapshot`, `restore`, `respond_approval`, and `command`.
|
|
35
51
|
- The lockstep session clock counts advances, so reset and restore, which now move the tick, never move it backward (AWP-TIM-013). Audit records hash frame payloads, not their base64 text.
|
|
36
|
-
|
|
37
52
|
- `awp`: `ClientConnection.submit` refuses action types the session was not granted (AWP-AGT-003); `AsyncClient` treats three heartbeat intervals without a message from the world as a lost connection (AWP-SAF-002).
|
|
38
53
|
- `awp_sim`: channels in modalities the agent did not declare are not granted (AWP-AGM-001); granted rates honor the agent's `max_obs_rate_hz` (AWP-AGM-002); a connection without a session is closed after at least 15 s (AWP-SES-012); `--max-duration-ms`. The demo agent does nothing when `move_to_pose` is not granted.
|
|
39
|
-
|
|
40
|
-
## 0.1.0.dev0 — unreleased
|
|
41
|
-
|
|
42
|
-
First implementation, targeting specification revision `0.1-draft.5`.
|
|
43
|
-
|
|
44
|
-
- `awp`: sans-IO `ClientConnection`, asyncio `AsyncClient`, binary and inline frame codec, canonical schema validation (receiver and sender forms), the lifecycle transition table, clock synchronization.
|
|
45
|
-
- `awp_sim`: sans-IO world engine for lockstep and streaming, simulated arm, WebSocket server with bearer authentication, audit log, trace recorder, in-process loopback, twelve scripted scenarios, and the `awp-sim` command.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@AGENTS.md
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: awp-python
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.0a6
|
|
4
4
|
Summary: Agent World Protocol agent SDK for Python.
|
|
5
5
|
Project-URL: Specification, https://www.agentworldprotocol.com
|
|
6
6
|
Project-URL: Source, https://github.com/Hyperduality/awp-python
|
|
@@ -23,22 +23,25 @@ Description-Content-Type: text/markdown
|
|
|
23
23
|
|
|
24
24
|
The Python agent SDK for the [Agent World Protocol](https://www.agentworldprotocol.com). `ClientConnection` is the agent side of AWP as a sans-IO state machine, and `awp.aio.AsyncClient` drives it over a WebSocket.
|
|
25
25
|
|
|
26
|
-
It targets specification revision **`0.1-draft.
|
|
26
|
+
It targets specification revision **`0.1-draft.10`**, pinned as the `spec/` submodule. This is an alpha, so the API will change along with the draft. The reference world, [awp-sim](https://github.com/Hyperduality/awp-sim), is a separate package built on this one.
|
|
27
27
|
|
|
28
28
|
## Status
|
|
29
29
|
|
|
30
|
-

|
|
31
31
|
|
|
32
|
-
In both time models, the demo agent built on the client is **Core Agent: AWP-conformant against 0.1-draft.
|
|
32
|
+
In both time models, the demo agent built on the client is **Core Agent: AWP-conformant against 0.1-draft.10**. [awp-conformance](https://github.com/Hyperduality/awp-conformance) reports no failure and nothing untested, and [`conformance/`](conformance/README.md) holds the reports and the evidence for their manual rows.
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
34
|
+
Implemented:
|
|
35
|
+
|
|
36
|
+
- lockstep and streaming sessions, with `world.tick` advances that wait for every per-tick channel;
|
|
37
|
+
- multi-bind sessions (`embodiments`);
|
|
38
|
+
- inline and `ws` stream bindings, and the binary frame codec (every spec vector);
|
|
39
|
+
- the action lifecycle, checked against the spec's transition table, and idempotent resubmission;
|
|
40
|
+
- heartbeats, clock synchronization, receiver reports, and resumption with replay and acknowledgement;
|
|
41
|
+
- canonical schema validation in receiver and sender forms;
|
|
42
|
+
- task, approval and standing approvals, transfer, reset, snapshots, and command channels.
|
|
43
|
+
|
|
44
|
+
Not implemented: stream bindings other than inline and `ws` (`webrtc`, `webtransport`, `shm`, `grpc`).
|
|
42
45
|
|
|
43
46
|
## Install
|
|
44
47
|
|
|
@@ -74,9 +77,11 @@ async with AsyncClient(conn, "ws://127.0.0.1:8710") as client:
|
|
|
74
77
|
Without asyncio, feed `ClientConnection` the decoded messages and send whatever it queues:
|
|
75
78
|
|
|
76
79
|
```python
|
|
77
|
-
|
|
80
|
+
from awp import jsonrpc
|
|
81
|
+
|
|
82
|
+
events = conn.receive(jsonrpc.decode(text)) # typed events: ActionUpdated, FrameReceived, ...
|
|
78
83
|
for out in conn.outgoing(): # messages to send, in order
|
|
79
|
-
transport.send(
|
|
84
|
+
transport.send(jsonrpc.encode(out))
|
|
80
85
|
```
|
|
81
86
|
|
|
82
87
|
The connection:
|
|
@@ -103,7 +108,7 @@ src/awp/ client protocol layer (sans-IO), asyncio adapter, frame code
|
|
|
103
108
|
src/awp/_spec/ schemas and lifecycle table bundled from spec/ (scripts/sync_spec.py)
|
|
104
109
|
tests/ unit tests, and a scripted world that serves over WebSockets
|
|
105
110
|
conformance/ conformance reports, the manifests they were run against, and evidence
|
|
106
|
-
spec/ agent-world-protocol, pinned at spec-v0.1-draft.
|
|
111
|
+
spec/ agent-world-protocol, pinned at spec-v0.1-draft.10
|
|
107
112
|
```
|
|
108
113
|
|
|
109
114
|
## Development
|
|
@@ -118,12 +123,7 @@ uv run pytest --cov
|
|
|
118
123
|
uv run python scripts/sync_spec.py --check
|
|
119
124
|
```
|
|
120
125
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
1. Check out its tag in `spec/`.
|
|
124
|
-
2. Run `scripts/sync_spec.py`.
|
|
125
|
-
3. Update `SPEC_REVISION` in `src/awp/__init__.py`.
|
|
126
|
-
4. Fix whatever the tests report.
|
|
126
|
+
[AGENTS.md](AGENTS.md) covers moving to a new draft revision and releasing.
|
|
127
127
|
|
|
128
128
|
## License
|
|
129
129
|
|
|
@@ -2,22 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
The Python agent SDK for the [Agent World Protocol](https://www.agentworldprotocol.com). `ClientConnection` is the agent side of AWP as a sans-IO state machine, and `awp.aio.AsyncClient` drives it over a WebSocket.
|
|
4
4
|
|
|
5
|
-
It targets specification revision **`0.1-draft.
|
|
5
|
+
It targets specification revision **`0.1-draft.10`**, pinned as the `spec/` submodule. This is an alpha, so the API will change along with the draft. The reference world, [awp-sim](https://github.com/Hyperduality/awp-sim), is a separate package built on this one.
|
|
6
6
|
|
|
7
7
|
## Status
|
|
8
8
|
|
|
9
|
-

|
|
10
10
|
|
|
11
|
-
In both time models, the demo agent built on the client is **Core Agent: AWP-conformant against 0.1-draft.
|
|
11
|
+
In both time models, the demo agent built on the client is **Core Agent: AWP-conformant against 0.1-draft.10**. [awp-conformance](https://github.com/Hyperduality/awp-conformance) reports no failure and nothing untested, and [`conformance/`](conformance/README.md) holds the reports and the evidence for their manual rows.
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
13
|
+
Implemented:
|
|
14
|
+
|
|
15
|
+
- lockstep and streaming sessions, with `world.tick` advances that wait for every per-tick channel;
|
|
16
|
+
- multi-bind sessions (`embodiments`);
|
|
17
|
+
- inline and `ws` stream bindings, and the binary frame codec (every spec vector);
|
|
18
|
+
- the action lifecycle, checked against the spec's transition table, and idempotent resubmission;
|
|
19
|
+
- heartbeats, clock synchronization, receiver reports, and resumption with replay and acknowledgement;
|
|
20
|
+
- canonical schema validation in receiver and sender forms;
|
|
21
|
+
- task, approval and standing approvals, transfer, reset, snapshots, and command channels.
|
|
22
|
+
|
|
23
|
+
Not implemented: stream bindings other than inline and `ws` (`webrtc`, `webtransport`, `shm`, `grpc`).
|
|
21
24
|
|
|
22
25
|
## Install
|
|
23
26
|
|
|
@@ -53,9 +56,11 @@ async with AsyncClient(conn, "ws://127.0.0.1:8710") as client:
|
|
|
53
56
|
Without asyncio, feed `ClientConnection` the decoded messages and send whatever it queues:
|
|
54
57
|
|
|
55
58
|
```python
|
|
56
|
-
|
|
59
|
+
from awp import jsonrpc
|
|
60
|
+
|
|
61
|
+
events = conn.receive(jsonrpc.decode(text)) # typed events: ActionUpdated, FrameReceived, ...
|
|
57
62
|
for out in conn.outgoing(): # messages to send, in order
|
|
58
|
-
transport.send(
|
|
63
|
+
transport.send(jsonrpc.encode(out))
|
|
59
64
|
```
|
|
60
65
|
|
|
61
66
|
The connection:
|
|
@@ -82,7 +87,7 @@ src/awp/ client protocol layer (sans-IO), asyncio adapter, frame code
|
|
|
82
87
|
src/awp/_spec/ schemas and lifecycle table bundled from spec/ (scripts/sync_spec.py)
|
|
83
88
|
tests/ unit tests, and a scripted world that serves over WebSockets
|
|
84
89
|
conformance/ conformance reports, the manifests they were run against, and evidence
|
|
85
|
-
spec/ agent-world-protocol, pinned at spec-v0.1-draft.
|
|
90
|
+
spec/ agent-world-protocol, pinned at spec-v0.1-draft.10
|
|
86
91
|
```
|
|
87
92
|
|
|
88
93
|
## Development
|
|
@@ -97,12 +102,7 @@ uv run pytest --cov
|
|
|
97
102
|
uv run python scripts/sync_spec.py --check
|
|
98
103
|
```
|
|
99
104
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
1. Check out its tag in `spec/`.
|
|
103
|
-
2. Run `scripts/sync_spec.py`.
|
|
104
|
-
3. Update `SPEC_REVISION` in `src/awp/__init__.py`.
|
|
105
|
-
4. Fix whatever the tests report.
|
|
105
|
+
[AGENTS.md](AGENTS.md) covers moving to a new draft revision and releasing.
|
|
106
106
|
|
|
107
107
|
## License
|
|
108
108
|
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
# Conformance
|
|
2
2
|
|
|
3
|
-

|
|
4
4
|
|
|
5
5
|
| Class | Configuration | Claim | Report |
|
|
6
6
|
|---|---|---|---|
|
|
7
|
-
| Core Agent | `awp-demo`, [`manifest-lockstep.json`](manifest-lockstep.json) | Core Agent (lockstep): AWP-conformant against 0.1-draft.
|
|
8
|
-
| Core Agent | `awp-demo`, [`manifest-streaming.json`](manifest-streaming.json) | Core Agent (streaming): AWP-conformant against 0.1-draft.
|
|
7
|
+
| Core Agent | `awp-demo`, [`manifest-lockstep.json`](manifest-lockstep.json) | Core Agent (lockstep): AWP-conformant against 0.1-draft.10 (awp-conformance 0.1.0a7) | [`core-agent-lockstep.json`](core-agent-lockstep.json) |
|
|
8
|
+
| Core Agent | `awp-demo`, [`manifest-streaming.json`](manifest-streaming.json) | Core Agent (streaming): AWP-conformant against 0.1-draft.10 (awp-conformance 0.1.0a7) | [`core-agent-streaming.json`](core-agent-streaming.json) |
|
|
9
9
|
|
|
10
|
-
The reports come from awp-conformance 0.1.
|
|
10
|
+
The reports come from awp-conformance 0.1.0a7 run against awp-python 0.1.0a6. Neither has a failure or anything untested. The evidence for their `manual` rows is below (AWP-CNF-005).
|
|
11
11
|
|
|
12
12
|
## Reproduce
|
|
13
13
|
|
|
@@ -19,7 +19,7 @@ awp-conformance agent --manifest conformance/manifest-lockstep.json --frames con
|
|
|
19
19
|
--mode lockstep --out report/ -- awp-demo --url '{url}' --token '{token}'
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
For streaming, use `manifest-streaming.json` with `--mode streaming`.
|
|
22
|
+
For streaming, use `manifest-streaming.json` with `--mode streaming`. The tests the evidence below cites:
|
|
23
23
|
|
|
24
24
|
```bash
|
|
25
25
|
uv run pytest tests/test_frames.py tests/test_evidence.py
|
|
@@ -38,16 +38,16 @@ In streaming, the suite reads the agent's loss accounting from its `obs.report`.
|
|
|
38
38
|
|
|
39
39
|
### AWP-DAT-008: frame test vectors
|
|
40
40
|
|
|
41
|
-
`test_vectors` (`tests/test_frames.py`) runs `awp.frames` over every vector in `schemas/test-vectors/frames.json` of the `spec/` submodule, which is pinned at `spec-v0.1-draft.
|
|
41
|
+
`test_vectors` (`tests/test_frames.py`) runs `awp.frames` over every vector in `schemas/test-vectors/frames.json` of the `spec/` submodule, which is pinned at `spec-v0.1-draft.10`:
|
|
42
42
|
|
|
43
43
|
- the 10 valid vectors decode to their listed fields;
|
|
44
|
-
- the
|
|
44
|
+
- the 10 marked `expect_error` are rejected with their listed error.
|
|
45
45
|
|
|
46
46
|
`test_roundtrip_without_vendor` re-encodes the 8 valid vectors that have no unknown extension or reserved bits, and each matches byte for byte.
|
|
47
47
|
|
|
48
48
|
### AWP-VER-009: the draft revision is named
|
|
49
49
|
|
|
50
|
-
- The [README](../README.md) and the [Python SDK page](https://www.agentworldprotocol.com/sdks/python) name `0.1-draft.
|
|
51
|
-
- `awp.SPEC_REVISION` is `"0.1-draft.
|
|
52
|
-
- The `spec/` submodule is pinned at the tag `spec-v0.1-draft.
|
|
53
|
-
- Each report records `"specification": "0.1-draft.
|
|
50
|
+
- The [README](../README.md) and the [Python SDK page](https://www.agentworldprotocol.com/sdks/python) name `0.1-draft.10`.
|
|
51
|
+
- `awp.SPEC_REVISION` is `"0.1-draft.10"`.
|
|
52
|
+
- The `spec/` submodule is pinned at the tag `spec-v0.1-draft.10`.
|
|
53
|
+
- Each report records `"specification": "0.1-draft.10"`, and its claim names the revision.
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"suite": {
|
|
3
3
|
"name": "awp-conformance",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.0a7"
|
|
5
5
|
},
|
|
6
|
-
"specification": "0.1-draft.
|
|
6
|
+
"specification": "0.1-draft.10",
|
|
7
7
|
"target": "awp-demo --url {url} --token {token}",
|
|
8
8
|
"side": "agent",
|
|
9
|
-
"generated": "2026-09-
|
|
9
|
+
"generated": "2026-09-26T22:32:00Z",
|
|
10
10
|
"claim": {
|
|
11
11
|
"status": "conformant",
|
|
12
|
-
"wording": "Core Agent (lockstep): AWP-conformant against 0.1-draft.
|
|
12
|
+
"wording": "Core Agent (lockstep): AWP-conformant against 0.1-draft.10 (awp-conformance 0.1.0a7)",
|
|
13
13
|
"classes": [
|
|
14
14
|
"Core Agent"
|
|
15
15
|
],
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
],
|
|
27
27
|
"counts": {
|
|
28
28
|
"manual": 4,
|
|
29
|
-
"n/a":
|
|
29
|
+
"n/a": 197,
|
|
30
30
|
"pass": 37,
|
|
31
31
|
"untestable": 3
|
|
32
32
|
}
|
|
@@ -138,7 +138,7 @@
|
|
|
138
138
|
"id": "AWP-AGT-006",
|
|
139
139
|
"level": "MUST",
|
|
140
140
|
"outcome": "pass",
|
|
141
|
-
"detail": "23 check(s): lockstep/baseline, lockstep/malformed-frame, lockstep/reconnect, lockstep/restarted-world, lockstep/silent-world"
|
|
141
|
+
"detail": "23 check(s): lockstep/baseline, lockstep/integer-range, lockstep/malformed-frame, lockstep/reconnect, lockstep/restarted-world, lockstep/silent-world"
|
|
142
142
|
},
|
|
143
143
|
{
|
|
144
144
|
"id": "AWP-AGT-007",
|
|
@@ -356,6 +356,12 @@
|
|
|
356
356
|
"outcome": "n/a",
|
|
357
357
|
"detail": "world-side requirement"
|
|
358
358
|
},
|
|
359
|
+
{
|
|
360
|
+
"id": "AWP-CMD-009",
|
|
361
|
+
"level": "SHOULD",
|
|
362
|
+
"outcome": "n/a",
|
|
363
|
+
"detail": "world-side requirement"
|
|
364
|
+
},
|
|
359
365
|
{
|
|
360
366
|
"id": "AWP-CNF-001",
|
|
361
367
|
"level": "MUST",
|
|
@@ -596,6 +602,12 @@
|
|
|
596
602
|
"outcome": "n/a",
|
|
597
603
|
"detail": "world-side requirement"
|
|
598
604
|
},
|
|
605
|
+
{
|
|
606
|
+
"id": "AWP-EVT-004",
|
|
607
|
+
"level": "MUST",
|
|
608
|
+
"outcome": "n/a",
|
|
609
|
+
"detail": "world-side requirement"
|
|
610
|
+
},
|
|
599
611
|
{
|
|
600
612
|
"id": "AWP-GUI-001",
|
|
601
613
|
"level": "MUST",
|
|
@@ -980,6 +992,12 @@
|
|
|
980
992
|
"outcome": "n/a",
|
|
981
993
|
"detail": "world-side requirement"
|
|
982
994
|
},
|
|
995
|
+
{
|
|
996
|
+
"id": "AWP-ROB-008",
|
|
997
|
+
"level": "SHOULD",
|
|
998
|
+
"outcome": "n/a",
|
|
999
|
+
"detail": "world-side requirement"
|
|
1000
|
+
},
|
|
983
1001
|
{
|
|
984
1002
|
"id": "AWP-SAF-001",
|
|
985
1003
|
"level": "MUST",
|
|
@@ -1206,7 +1224,7 @@
|
|
|
1206
1224
|
"id": "AWP-TIM-003",
|
|
1207
1225
|
"level": "MUST",
|
|
1208
1226
|
"outcome": "pass",
|
|
1209
|
-
"detail": "
|
|
1227
|
+
"detail": "7 check(s): lockstep/baseline, lockstep/malformed-frame, lockstep/reconnect, lockstep/restarted-world"
|
|
1210
1228
|
},
|
|
1211
1229
|
{
|
|
1212
1230
|
"id": "AWP-TIM-004",
|
|
@@ -1268,6 +1286,12 @@
|
|
|
1268
1286
|
"outcome": "n/a",
|
|
1269
1287
|
"detail": "world-side requirement"
|
|
1270
1288
|
},
|
|
1289
|
+
{
|
|
1290
|
+
"id": "AWP-TIM-014",
|
|
1291
|
+
"level": "MUST",
|
|
1292
|
+
"outcome": "n/a",
|
|
1293
|
+
"detail": "world-side requirement"
|
|
1294
|
+
},
|
|
1271
1295
|
{
|
|
1272
1296
|
"id": "AWP-TRN-001",
|
|
1273
1297
|
"level": "MUST",
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"suite": {
|
|
3
3
|
"name": "awp-conformance",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.0a7"
|
|
5
5
|
},
|
|
6
|
-
"specification": "0.1-draft.
|
|
6
|
+
"specification": "0.1-draft.10",
|
|
7
7
|
"target": "awp-demo --url {url} --token {token}",
|
|
8
8
|
"side": "agent",
|
|
9
|
-
"generated": "2026-09-
|
|
9
|
+
"generated": "2026-09-26T22:32:02Z",
|
|
10
10
|
"claim": {
|
|
11
11
|
"status": "conformant",
|
|
12
|
-
"wording": "Core Agent (streaming): AWP-conformant against 0.1-draft.
|
|
12
|
+
"wording": "Core Agent (streaming): AWP-conformant against 0.1-draft.10 (awp-conformance 0.1.0a7)",
|
|
13
13
|
"classes": [
|
|
14
14
|
"Core Agent"
|
|
15
15
|
],
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
],
|
|
25
25
|
"counts": {
|
|
26
26
|
"manual": 2,
|
|
27
|
-
"n/a":
|
|
27
|
+
"n/a": 191,
|
|
28
28
|
"pass": 45,
|
|
29
29
|
"untestable": 3
|
|
30
30
|
}
|
|
@@ -354,6 +354,12 @@
|
|
|
354
354
|
"outcome": "n/a",
|
|
355
355
|
"detail": "world-side requirement"
|
|
356
356
|
},
|
|
357
|
+
{
|
|
358
|
+
"id": "AWP-CMD-009",
|
|
359
|
+
"level": "SHOULD",
|
|
360
|
+
"outcome": "n/a",
|
|
361
|
+
"detail": "world-side requirement"
|
|
362
|
+
},
|
|
357
363
|
{
|
|
358
364
|
"id": "AWP-CNF-001",
|
|
359
365
|
"level": "MUST",
|
|
@@ -594,6 +600,12 @@
|
|
|
594
600
|
"outcome": "n/a",
|
|
595
601
|
"detail": "world-side requirement"
|
|
596
602
|
},
|
|
603
|
+
{
|
|
604
|
+
"id": "AWP-EVT-004",
|
|
605
|
+
"level": "MUST",
|
|
606
|
+
"outcome": "n/a",
|
|
607
|
+
"detail": "world-side requirement"
|
|
608
|
+
},
|
|
597
609
|
{
|
|
598
610
|
"id": "AWP-GUI-001",
|
|
599
611
|
"level": "MUST",
|
|
@@ -978,6 +990,12 @@
|
|
|
978
990
|
"outcome": "n/a",
|
|
979
991
|
"detail": "world-side requirement"
|
|
980
992
|
},
|
|
993
|
+
{
|
|
994
|
+
"id": "AWP-ROB-008",
|
|
995
|
+
"level": "SHOULD",
|
|
996
|
+
"outcome": "n/a",
|
|
997
|
+
"detail": "world-side requirement"
|
|
998
|
+
},
|
|
981
999
|
{
|
|
982
1000
|
"id": "AWP-SAF-001",
|
|
983
1001
|
"level": "MUST",
|
|
@@ -1266,6 +1284,12 @@
|
|
|
1266
1284
|
"outcome": "n/a",
|
|
1267
1285
|
"detail": "world-side requirement"
|
|
1268
1286
|
},
|
|
1287
|
+
{
|
|
1288
|
+
"id": "AWP-TIM-014",
|
|
1289
|
+
"level": "MUST",
|
|
1290
|
+
"outcome": "n/a",
|
|
1291
|
+
"detail": "world-side requirement"
|
|
1292
|
+
},
|
|
1269
1293
|
{
|
|
1270
1294
|
"id": "AWP-TRN-001",
|
|
1271
1295
|
"level": "MUST",
|
|
@@ -28,6 +28,7 @@ from .client import (
|
|
|
28
28
|
Event,
|
|
29
29
|
FrameReceived,
|
|
30
30
|
Response,
|
|
31
|
+
SessionOpened,
|
|
31
32
|
SessionStateChanged,
|
|
32
33
|
)
|
|
33
34
|
from .errors import AwpError, ErrorCode
|
|
@@ -40,9 +41,11 @@ SUBPROTOCOL = Subprotocol("awp")
|
|
|
40
41
|
class AsyncClient:
|
|
41
42
|
"""An agent's connection to a world.
|
|
42
43
|
|
|
43
|
-
|
|
44
|
+
A timer sends pings. In a streaming session, an agent that actuates something should pass
|
|
44
45
|
`heartbeat=False` and call `ping()` from its decision loop instead, so that a stalled policy
|
|
45
|
-
also stops the heartbeat that holds off the world's watchdog (AWP-SAF-005).
|
|
46
|
+
also stops the heartbeat that holds off the world's watchdog (AWP-SAF-005). A lockstep session
|
|
47
|
+
has no watchdog, and its `advance()` may wait on other sessions (AWP-TIM-012), so the timer
|
|
48
|
+
pings there regardless.
|
|
46
49
|
"""
|
|
47
50
|
|
|
48
51
|
def __init__(
|
|
@@ -95,8 +98,7 @@ class AsyncClient:
|
|
|
95
98
|
asyncio.create_task(self._read(self._ws)),
|
|
96
99
|
asyncio.create_task(self._watch(self._ws)),
|
|
97
100
|
]
|
|
98
|
-
|
|
99
|
-
self._tasks.append(asyncio.create_task(self._beat()))
|
|
101
|
+
self._tasks.append(asyncio.create_task(self._beat()))
|
|
100
102
|
if self.report_interval_s:
|
|
101
103
|
self._tasks.append(asyncio.create_task(self._report()))
|
|
102
104
|
|
|
@@ -139,9 +141,9 @@ class AsyncClient:
|
|
|
139
141
|
async def flush(self) -> None:
|
|
140
142
|
"""Send everything the connection has queued, in order."""
|
|
141
143
|
async with self._send_lock:
|
|
144
|
+
if self._ws is None:
|
|
145
|
+
raise ConnectionError("not connected")
|
|
142
146
|
for msg in self.conn.outgoing():
|
|
143
|
-
if self._ws is None:
|
|
144
|
-
raise ConnectionError("not connected")
|
|
145
147
|
await self._ws.send(jsonrpc.encode(msg))
|
|
146
148
|
|
|
147
149
|
async def _drop(self) -> None:
|
|
@@ -250,6 +252,8 @@ class AsyncClient:
|
|
|
250
252
|
def _dispatch(self, event: Event) -> None:
|
|
251
253
|
if isinstance(event, FrameReceived):
|
|
252
254
|
self.latest[event.channel] = event
|
|
255
|
+
elif isinstance(event, SessionOpened) and not event.resumed:
|
|
256
|
+
self.latest.clear()
|
|
253
257
|
for q in self._subscribers:
|
|
254
258
|
q.put_nowait(event)
|
|
255
259
|
for item in list(self._waiters):
|
|
@@ -263,7 +267,9 @@ class AsyncClient:
|
|
|
263
267
|
last = loop.time()
|
|
264
268
|
while not self._closed.is_set():
|
|
265
269
|
await asyncio.sleep(0.05) # re-read the interval: it is only known once a session opens
|
|
266
|
-
if self.conn.ready is
|
|
270
|
+
if self.conn.ready is None or not (self.heartbeat or self.conn.lockstep):
|
|
271
|
+
continue
|
|
272
|
+
if loop.time() - last >= self.conn.heartbeat_s:
|
|
267
273
|
last = loop.time()
|
|
268
274
|
self.conn.ping()
|
|
269
275
|
await self.flush()
|
|
@@ -275,25 +281,17 @@ class AsyncClient:
|
|
|
275
281
|
await asyncio.sleep(0.1)
|
|
276
282
|
if self.conn.ready is None:
|
|
277
283
|
continue
|
|
278
|
-
interval =
|
|
284
|
+
interval = self.conn.ready["heartbeat_interval_ms"] / 1000
|
|
279
285
|
if loop.time() - self._last_rx > 3 * interval:
|
|
280
286
|
log.warning("no message from the world for %.1f s; closing", 3 * interval)
|
|
281
287
|
await ws.close(code=1001, reason="heartbeat lost")
|
|
282
288
|
return
|
|
283
289
|
|
|
284
|
-
def _heartbeat_s(self) -> float:
|
|
285
|
-
"""Every heartbeat interval, and at least twice per watchdog period (AWP-SAF-005)."""
|
|
286
|
-
interval = (self.conn.ready or {}).get("heartbeat_interval_ms", 5000)
|
|
287
|
-
safe_state = ((self.conn.manifest or {}).get("safety_policy") or {}).get("safe_state")
|
|
288
|
-
if safe_state:
|
|
289
|
-
interval = min(interval, safe_state["watchdog_ms"] / 2)
|
|
290
|
-
return float(interval) / 1000
|
|
291
|
-
|
|
292
290
|
async def _report(self) -> None:
|
|
293
291
|
assert self.report_interval_s is not None
|
|
294
292
|
while not self._closed.is_set():
|
|
295
293
|
await asyncio.sleep(self.report_interval_s)
|
|
296
|
-
streaming = self.conn.
|
|
294
|
+
streaming = self.conn.ready is not None and not self.conn.lockstep
|
|
297
295
|
if streaming and self.conn.clock.samples:
|
|
298
296
|
self.conn.report()
|
|
299
297
|
await self.flush()
|
|
@@ -308,24 +306,23 @@ class AsyncClient:
|
|
|
308
306
|
self._waiters.append((predicate, fut))
|
|
309
307
|
return fut
|
|
310
308
|
|
|
311
|
-
async def _await(self, fut: asyncio.Future[Event], timeout: float) -> Event:
|
|
309
|
+
async def _await(self, fut: asyncio.Future[Event], timeout: float | None) -> Event:
|
|
312
310
|
try:
|
|
313
311
|
return await asyncio.wait_for(fut, timeout)
|
|
314
312
|
finally:
|
|
315
313
|
self._waiters = [(p, f) for p, f in self._waiters if f is not fut]
|
|
316
314
|
|
|
317
|
-
async def wait_for(
|
|
315
|
+
async def wait_for(
|
|
316
|
+
self, predicate: Callable[[Event], bool], timeout: float | None = 10.0
|
|
317
|
+
) -> Event:
|
|
318
318
|
return await self._await(self._waiter(predicate), timeout)
|
|
319
319
|
|
|
320
|
-
async def call(self, rid: int, timeout: float = 10.0) -> dict[str, Any]:
|
|
321
|
-
"""Send what is queued and await the response to request `rid`.
|
|
320
|
+
async def call(self, rid: int, timeout: float | None = 10.0) -> dict[str, Any]:
|
|
321
|
+
"""Send what is queued and await the response to request `rid`. A timeout stops the
|
|
322
|
+
waiting, not the request: a late response is still processed."""
|
|
322
323
|
fut = self._waiter(lambda e: isinstance(e, (Response, ErrorResponse)) and e.id == rid)
|
|
323
324
|
await self.flush()
|
|
324
|
-
|
|
325
|
-
event = await self._await(fut, timeout)
|
|
326
|
-
except TimeoutError:
|
|
327
|
-
self.conn.forget(rid)
|
|
328
|
-
raise
|
|
325
|
+
event = await self._await(fut, timeout)
|
|
329
326
|
if isinstance(event, ErrorResponse):
|
|
330
327
|
raise event.error
|
|
331
328
|
assert isinstance(event, Response)
|
|
@@ -387,7 +384,7 @@ class AsyncClient:
|
|
|
387
384
|
async def cancel(self, action_id: str) -> dict[str, Any]:
|
|
388
385
|
return await self.call(self.conn.cancel(action_id))
|
|
389
386
|
|
|
390
|
-
async def advance(self, count: int | None = None, timeout: float = 10.0) -> int:
|
|
387
|
+
async def advance(self, count: int | None = None, timeout: float | None = 10.0) -> int:
|
|
391
388
|
"""Advance lockstep time. Returns once the result and a frame of its tick on every
|
|
392
389
|
subscribed per-tick channel are held; on a stream connection the frames may follow the
|
|
393
390
|
result (AWP-TIM-003)."""
|