awp-python 0.1.0a4__tar.gz → 0.1.0a5__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.
Files changed (80) hide show
  1. awp_python-0.1.0a5/AGENTS.md +58 -0
  2. {awp_python-0.1.0a4 → awp_python-0.1.0a5}/CHANGELOG.md +10 -8
  3. awp_python-0.1.0a5/CLAUDE.md +1 -0
  4. {awp_python-0.1.0a4 → awp_python-0.1.0a5}/PKG-INFO +18 -18
  5. {awp_python-0.1.0a4 → awp_python-0.1.0a5}/README.md +17 -17
  6. {awp_python-0.1.0a4 → awp_python-0.1.0a5}/conformance/README.md +5 -5
  7. {awp_python-0.1.0a4 → awp_python-0.1.0a5}/conformance/core-agent-lockstep.json +10 -10
  8. {awp_python-0.1.0a4 → awp_python-0.1.0a5}/conformance/core-agent-streaming.json +3 -3
  9. {awp_python-0.1.0a4 → awp_python-0.1.0a5}/pyproject.toml +1 -1
  10. {awp_python-0.1.0a4 → awp_python-0.1.0a5}/src/awp/aio.py +24 -27
  11. {awp_python-0.1.0a4 → awp_python-0.1.0a5}/src/awp/client.py +86 -32
  12. {awp_python-0.1.0a4 → awp_python-0.1.0a5}/src/awp/schema.py +7 -9
  13. {awp_python-0.1.0a4 → awp_python-0.1.0a5}/tests/test_aio.py +12 -0
  14. awp_python-0.1.0a5/tests/test_client.py +87 -0
  15. {awp_python-0.1.0a4 → awp_python-0.1.0a5}/tests/test_client_traces.py +1 -3
  16. {awp_python-0.1.0a4 → awp_python-0.1.0a5}/tests/test_schema.py +12 -9
  17. {awp_python-0.1.0a4 → awp_python-0.1.0a5}/tests/world.py +2 -2
  18. {awp_python-0.1.0a4 → awp_python-0.1.0a5}/uv.lock +1 -1
  19. awp_python-0.1.0a4/tests/test_client.py +0 -31
  20. {awp_python-0.1.0a4 → awp_python-0.1.0a5}/.gitignore +0 -0
  21. {awp_python-0.1.0a4 → awp_python-0.1.0a5}/.gitmodules +0 -0
  22. {awp_python-0.1.0a4 → awp_python-0.1.0a5}/.python-version +0 -0
  23. {awp_python-0.1.0a4 → awp_python-0.1.0a5}/LICENSE +0 -0
  24. {awp_python-0.1.0a4 → awp_python-0.1.0a5}/conformance/frames.json +0 -0
  25. {awp_python-0.1.0a4 → awp_python-0.1.0a5}/conformance/manifest-lockstep.json +0 -0
  26. {awp_python-0.1.0a4 → awp_python-0.1.0a5}/conformance/manifest-streaming.json +0 -0
  27. {awp_python-0.1.0a4 → awp_python-0.1.0a5}/scripts/sync_spec.py +0 -0
  28. {awp_python-0.1.0a4 → awp_python-0.1.0a5}/src/awp/__init__.py +0 -0
  29. {awp_python-0.1.0a4 → awp_python-0.1.0a5}/src/awp/_spec/lifecycle.json +0 -0
  30. {awp_python-0.1.0a4 → awp_python-0.1.0a5}/src/awp/_spec/schemas/action-cancel-result.schema.json +0 -0
  31. {awp_python-0.1.0a4 → awp_python-0.1.0a5}/src/awp/_spec/schemas/action-ref.schema.json +0 -0
  32. {awp_python-0.1.0a4 → awp_python-0.1.0a5}/src/awp/_spec/schemas/action-schema.schema.json +0 -0
  33. {awp_python-0.1.0a4 → awp_python-0.1.0a5}/src/awp/_spec/schemas/action-status.schema.json +0 -0
  34. {awp_python-0.1.0a4 → awp_python-0.1.0a5}/src/awp/_spec/schemas/action-submit-result.schema.json +0 -0
  35. {awp_python-0.1.0a4 → awp_python-0.1.0a5}/src/awp/_spec/schemas/action-submit.schema.json +0 -0
  36. {awp_python-0.1.0a4 → awp_python-0.1.0a5}/src/awp/_spec/schemas/agent-manifest.schema.json +0 -0
  37. {awp_python-0.1.0a4 → awp_python-0.1.0a5}/src/awp/_spec/schemas/approval-requested.schema.json +0 -0
  38. {awp_python-0.1.0a4 → awp_python-0.1.0a5}/src/awp/_spec/schemas/approval-respond.schema.json +0 -0
  39. {awp_python-0.1.0a4 → awp_python-0.1.0a5}/src/awp/_spec/schemas/common.schema.json +0 -0
  40. {awp_python-0.1.0a4 → awp_python-0.1.0a5}/src/awp/_spec/schemas/embodiment.schema.json +0 -0
  41. {awp_python-0.1.0a4 → awp_python-0.1.0a5}/src/awp/_spec/schemas/empty-result.schema.json +0 -0
  42. {awp_python-0.1.0a4 → awp_python-0.1.0a5}/src/awp/_spec/schemas/error.schema.json +0 -0
  43. {awp_python-0.1.0a4 → awp_python-0.1.0a5}/src/awp/_spec/schemas/frame-inline.schema.json +0 -0
  44. {awp_python-0.1.0a4 → awp_python-0.1.0a5}/src/awp/_spec/schemas/frame-tree.schema.json +0 -0
  45. {awp_python-0.1.0a4 → awp_python-0.1.0a5}/src/awp/_spec/schemas/obs-report.schema.json +0 -0
  46. {awp_python-0.1.0a4 → awp_python-0.1.0a5}/src/awp/_spec/schemas/observation-channel.schema.json +0 -0
  47. {awp_python-0.1.0a4 → awp_python-0.1.0a5}/src/awp/_spec/schemas/ping-result.schema.json +0 -0
  48. {awp_python-0.1.0a4 → awp_python-0.1.0a5}/src/awp/_spec/schemas/ping.schema.json +0 -0
  49. {awp_python-0.1.0a4 → awp_python-0.1.0a5}/src/awp/_spec/schemas/profiles/gui-actions.schema.json +0 -0
  50. {awp_python-0.1.0a4 → awp_python-0.1.0a5}/src/awp/_spec/schemas/reset-result.schema.json +0 -0
  51. {awp_python-0.1.0a4 → awp_python-0.1.0a5}/src/awp/_spec/schemas/reset.schema.json +0 -0
  52. {awp_python-0.1.0a4 → awp_python-0.1.0a5}/src/awp/_spec/schemas/restore.schema.json +0 -0
  53. {awp_python-0.1.0a4 → awp_python-0.1.0a5}/src/awp/_spec/schemas/safety-policy.schema.json +0 -0
  54. {awp_python-0.1.0a4 → awp_python-0.1.0a5}/src/awp/_spec/schemas/session-open.schema.json +0 -0
  55. {awp_python-0.1.0a4 → awp_python-0.1.0a5}/src/awp/_spec/schemas/session-ready.schema.json +0 -0
  56. {awp_python-0.1.0a4 → awp_python-0.1.0a5}/src/awp/_spec/schemas/session-resume.schema.json +0 -0
  57. {awp_python-0.1.0a4 → awp_python-0.1.0a5}/src/awp/_spec/schemas/session-state.schema.json +0 -0
  58. {awp_python-0.1.0a4 → awp_python-0.1.0a5}/src/awp/_spec/schemas/session-telemetry.schema.json +0 -0
  59. {awp_python-0.1.0a4 → awp_python-0.1.0a5}/src/awp/_spec/schemas/session-transfer-result.schema.json +0 -0
  60. {awp_python-0.1.0a4 → awp_python-0.1.0a5}/src/awp/_spec/schemas/session-transfer.schema.json +0 -0
  61. {awp_python-0.1.0a4 → awp_python-0.1.0a5}/src/awp/_spec/schemas/snapshot-result.schema.json +0 -0
  62. {awp_python-0.1.0a4 → awp_python-0.1.0a5}/src/awp/_spec/schemas/subscribe-result.schema.json +0 -0
  63. {awp_python-0.1.0a4 → awp_python-0.1.0a5}/src/awp/_spec/schemas/subscribe.schema.json +0 -0
  64. {awp_python-0.1.0a4 → awp_python-0.1.0a5}/src/awp/_spec/schemas/task-update.schema.json +0 -0
  65. {awp_python-0.1.0a4 → awp_python-0.1.0a5}/src/awp/_spec/schemas/tick-result.schema.json +0 -0
  66. {awp_python-0.1.0a4 → awp_python-0.1.0a5}/src/awp/_spec/schemas/tick.schema.json +0 -0
  67. {awp_python-0.1.0a4 → awp_python-0.1.0a5}/src/awp/_spec/schemas/unsubscribe.schema.json +0 -0
  68. {awp_python-0.1.0a4 → awp_python-0.1.0a5}/src/awp/_spec/schemas/world-event.schema.json +0 -0
  69. {awp_python-0.1.0a4 → awp_python-0.1.0a5}/src/awp/_spec/schemas/world-manifest.schema.json +0 -0
  70. {awp_python-0.1.0a4 → awp_python-0.1.0a5}/src/awp/clock.py +0 -0
  71. {awp_python-0.1.0a4 → awp_python-0.1.0a5}/src/awp/demo.py +0 -0
  72. {awp_python-0.1.0a4 → awp_python-0.1.0a5}/src/awp/errors.py +0 -0
  73. {awp_python-0.1.0a4 → awp_python-0.1.0a5}/src/awp/frames.py +0 -0
  74. {awp_python-0.1.0a4 → awp_python-0.1.0a5}/src/awp/jsonrpc.py +0 -0
  75. {awp_python-0.1.0a4 → awp_python-0.1.0a5}/src/awp/lifecycle.py +0 -0
  76. {awp_python-0.1.0a4 → awp_python-0.1.0a5}/src/awp/py.typed +0 -0
  77. {awp_python-0.1.0a4 → awp_python-0.1.0a5}/tests/__init__.py +0 -0
  78. {awp_python-0.1.0a4 → awp_python-0.1.0a5}/tests/conftest.py +0 -0
  79. {awp_python-0.1.0a4 → awp_python-0.1.0a5}/tests/test_evidence.py +0 -0
  80. {awp_python-0.1.0a4 → awp_python-0.1.0a5}/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,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.0a5
4
+
5
+ Targets specification revision `0.1-draft.9`.
6
+
7
+ - `open_session` takes `embodiments` to bind several embodiments of one `multi_bind_group`, and `submit` then requires `embodiment_id` (AWP-EMB-005).
8
+ - 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`.
9
+ - `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`.
10
+ - 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.
11
+ - `ClientConnection` gains `lockstep`, `heartbeat_s`, and `embodiments`. `forget` is gone.
12
+
3
13
  ## 0.1.0a4
4
14
 
5
15
  Targets specification revision `0.1-draft.9`.
@@ -33,13 +43,5 @@ First release, targeting specification revision `0.1-draft.7`.
33
43
  - 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
44
  - 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
45
  - 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
46
  - `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
47
  - `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.0a4
3
+ Version: 0.1.0a5
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
@@ -29,16 +29,19 @@ It targets specification revision **`0.1-draft.9`**, pinned as the `spec/` submo
29
29
 
30
30
  ![AWP: Core Agent, AWP-conformant against 0.1-draft.9](https://img.shields.io/badge/AWP-Core_Agent%2C_conformant_0.1--draft.9-555)
31
31
 
32
- In both time models, the demo agent built on the client is **Core Agent: AWP-conformant against 0.1-draft.9**. [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. CI runs the suite against the demo agent on every change.
32
+ In both time models, the demo agent built on the client is **Core Agent: AWP-conformant against 0.1-draft.9**. [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
- | Implemented | Not implemented |
35
- |---|---|
36
- | Lockstep and streaming sessions; `world.tick` advances that wait for every per-tick channel | Multi-bind (`embodiments`) |
37
- | Inline and `ws` stream bindings; binary frame codec (every spec vector) | Other stream bindings (`webrtc`, `webtransport`, `shm`, `grpc`) |
38
- | Action lifecycle checked against the spec's transition table; idempotent resubmission | |
39
- | Heartbeats, clock synchronization, receiver reports, resumption with replay and acknowledgement | |
40
- | Canonical schema validation in receiver and sender forms | |
41
- | Task, approval and standing approvals, transfer, reset, snapshots, and command channels | |
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
- events = conn.receive(message) # typed events: ActionUpdated, FrameReceived, ...
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(json.dumps(out))
84
+ transport.send(jsonrpc.encode(out))
80
85
  ```
81
86
 
82
87
  The connection:
@@ -118,12 +123,7 @@ uv run pytest --cov
118
123
  uv run python scripts/sync_spec.py --check
119
124
  ```
120
125
 
121
- To move to a new draft revision:
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
 
@@ -8,16 +8,19 @@ It targets specification revision **`0.1-draft.9`**, pinned as the `spec/` submo
8
8
 
9
9
  ![AWP: Core Agent, AWP-conformant against 0.1-draft.9](https://img.shields.io/badge/AWP-Core_Agent%2C_conformant_0.1--draft.9-555)
10
10
 
11
- In both time models, the demo agent built on the client is **Core Agent: AWP-conformant against 0.1-draft.9**. [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. CI runs the suite against the demo agent on every change.
11
+ In both time models, the demo agent built on the client is **Core Agent: AWP-conformant against 0.1-draft.9**. [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
- | Implemented | Not implemented |
14
- |---|---|
15
- | Lockstep and streaming sessions; `world.tick` advances that wait for every per-tick channel | Multi-bind (`embodiments`) |
16
- | Inline and `ws` stream bindings; binary frame codec (every spec vector) | Other stream bindings (`webrtc`, `webtransport`, `shm`, `grpc`) |
17
- | Action lifecycle checked against the spec's transition table; idempotent resubmission | |
18
- | Heartbeats, clock synchronization, receiver reports, resumption with replay and acknowledgement | |
19
- | Canonical schema validation in receiver and sender forms | |
20
- | Task, approval and standing approvals, transfer, reset, snapshots, and command channels | |
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
- events = conn.receive(message) # typed events: ActionUpdated, FrameReceived, ...
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(json.dumps(out))
63
+ transport.send(jsonrpc.encode(out))
59
64
  ```
60
65
 
61
66
  The connection:
@@ -97,12 +102,7 @@ uv run pytest --cov
97
102
  uv run python scripts/sync_spec.py --check
98
103
  ```
99
104
 
100
- To move to a new draft revision:
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
 
@@ -4,10 +4,10 @@
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.9 (awp-conformance 0.1.0a3) | [`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.9 (awp-conformance 0.1.0a3) | [`core-agent-streaming.json`](core-agent-streaming.json) |
7
+ | Core Agent | `awp-demo`, [`manifest-lockstep.json`](manifest-lockstep.json) | Core Agent (lockstep): AWP-conformant against 0.1-draft.9 (awp-conformance 0.1.0a6) | [`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.9 (awp-conformance 0.1.0a6) | [`core-agent-streaming.json`](core-agent-streaming.json) |
9
9
 
10
- The reports come from awp-conformance 0.1.0a3 run against awp-python 0.1.0a4. Neither has a failure or anything untested. The evidence for their `manual` rows is below (AWP-CNF-005).
10
+ The reports come from awp-conformance 0.1.0a6 run against awp-python 0.1.0a5. 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`. CI runs both on every change. The tests cited below run in CI too:
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
@@ -41,7 +41,7 @@ In streaming, the suite reads the agent's loss accounting from its `obs.report`.
41
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.9`:
42
42
 
43
43
  - the 10 valid vectors decode to their listed fields;
44
- - the 7 marked `expect_error` are rejected with their listed error.
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
 
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "suite": {
3
3
  "name": "awp-conformance",
4
- "version": "0.1.0a3"
4
+ "version": "0.1.0a6"
5
5
  },
6
6
  "specification": "0.1-draft.9",
7
7
  "target": "awp-demo --url {url} --token {token}",
8
8
  "side": "agent",
9
- "generated": "2026-09-25T21:46:19Z",
9
+ "generated": "2026-09-26T18:45:07Z",
10
10
  "claim": {
11
11
  "status": "conformant",
12
- "wording": "Core Agent (lockstep): AWP-conformant against 0.1-draft.9 (awp-conformance 0.1.0a3)",
12
+ "wording": "Core Agent (lockstep): AWP-conformant against 0.1-draft.9 (awp-conformance 0.1.0a6)",
13
13
  "classes": [
14
14
  "Core Agent"
15
15
  ],
@@ -108,7 +108,7 @@
108
108
  "id": "AWP-AGT-001",
109
109
  "level": "MUST",
110
110
  "outcome": "pass",
111
- "detail": "123 check(s): lockstep/baseline, lockstep/integer-range, lockstep/invalid-manifest, lockstep/malformed-frame, lockstep/no-action-grants, lockstep/reconnect, lockstep/refused-submission, lockstep/restarted-world, lockstep/silent-world"
111
+ "detail": "124 check(s): lockstep/baseline, lockstep/integer-range, lockstep/invalid-manifest, lockstep/malformed-frame, lockstep/no-action-grants, lockstep/reconnect, lockstep/refused-submission, lockstep/restarted-world, lockstep/silent-world"
112
112
  },
113
113
  {
114
114
  "id": "AWP-AGT-002",
@@ -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": "24 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",
@@ -396,7 +396,7 @@
396
396
  "id": "AWP-CTL-001",
397
397
  "level": "MUST",
398
398
  "outcome": "pass",
399
- "detail": "124 check(s): lockstep/baseline, lockstep/integer-range, lockstep/invalid-manifest, lockstep/malformed-frame, lockstep/no-action-grants, lockstep/reconnect, lockstep/refused-submission, lockstep/restarted-world, lockstep/silent-world"
399
+ "detail": "125 check(s): lockstep/baseline, lockstep/integer-range, lockstep/invalid-manifest, lockstep/malformed-frame, lockstep/no-action-grants, lockstep/reconnect, lockstep/refused-submission, lockstep/restarted-world, lockstep/silent-world"
400
400
  },
401
401
  {
402
402
  "id": "AWP-CTL-002",
@@ -426,7 +426,7 @@
426
426
  "id": "AWP-CTL-006",
427
427
  "level": "MUST",
428
428
  "outcome": "pass",
429
- "detail": "124 check(s): lockstep/baseline, lockstep/integer-range, lockstep/invalid-manifest, lockstep/malformed-frame, lockstep/no-action-grants, lockstep/reconnect, lockstep/refused-submission, lockstep/restarted-world, lockstep/silent-world"
429
+ "detail": "125 check(s): lockstep/baseline, lockstep/integer-range, lockstep/invalid-manifest, lockstep/malformed-frame, lockstep/no-action-grants, lockstep/reconnect, lockstep/refused-submission, lockstep/restarted-world, lockstep/silent-world"
430
430
  },
431
431
  {
432
432
  "id": "AWP-CTL-007",
@@ -444,7 +444,7 @@
444
444
  "id": "AWP-CTL-009",
445
445
  "level": "MUST",
446
446
  "outcome": "pass",
447
- "detail": "126 check(s): lockstep/baseline, lockstep/integer-range, lockstep/invalid-manifest, lockstep/malformed-frame, lockstep/no-action-grants, lockstep/reconnect, lockstep/refused-submission, lockstep/restarted-world, lockstep/silent-world"
447
+ "detail": "127 check(s): lockstep/baseline, lockstep/integer-range, lockstep/invalid-manifest, lockstep/malformed-frame, lockstep/no-action-grants, lockstep/reconnect, lockstep/refused-submission, lockstep/restarted-world, lockstep/silent-world"
448
448
  },
449
449
  {
450
450
  "id": "AWP-CTL-010",
@@ -1206,7 +1206,7 @@
1206
1206
  "id": "AWP-TIM-003",
1207
1207
  "level": "MUST",
1208
1208
  "outcome": "pass",
1209
- "detail": "5 check(s): lockstep/baseline, lockstep/malformed-frame, lockstep/reconnect, lockstep/restarted-world"
1209
+ "detail": "6 check(s): lockstep/baseline, lockstep/reconnect, lockstep/restarted-world"
1210
1210
  },
1211
1211
  {
1212
1212
  "id": "AWP-TIM-004",
@@ -1422,7 +1422,7 @@
1422
1422
  "id": "AWP-VER-004",
1423
1423
  "level": "MUST",
1424
1424
  "outcome": "pass",
1425
- "detail": "110 check(s): lockstep/baseline, lockstep/integer-range, lockstep/invalid-manifest, lockstep/malformed-frame, lockstep/no-action-grants, lockstep/reconnect, lockstep/refused-submission, lockstep/restarted-world, lockstep/silent-world"
1425
+ "detail": "111 check(s): lockstep/baseline, lockstep/integer-range, lockstep/invalid-manifest, lockstep/malformed-frame, lockstep/no-action-grants, lockstep/reconnect, lockstep/refused-submission, lockstep/restarted-world, lockstep/silent-world"
1426
1426
  },
1427
1427
  {
1428
1428
  "id": "AWP-VER-005",
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "suite": {
3
3
  "name": "awp-conformance",
4
- "version": "0.1.0a3"
4
+ "version": "0.1.0a6"
5
5
  },
6
6
  "specification": "0.1-draft.9",
7
7
  "target": "awp-demo --url {url} --token {token}",
8
8
  "side": "agent",
9
- "generated": "2026-09-25T21:46:39Z",
9
+ "generated": "2026-09-26T18:45:09Z",
10
10
  "claim": {
11
11
  "status": "conformant",
12
- "wording": "Core Agent (streaming): AWP-conformant against 0.1-draft.9 (awp-conformance 0.1.0a3)",
12
+ "wording": "Core Agent (streaming): AWP-conformant against 0.1-draft.9 (awp-conformance 0.1.0a6)",
13
13
  "classes": [
14
14
  "Core Agent"
15
15
  ],
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "awp-python"
7
- version = "0.1.0a4"
7
+ version = "0.1.0a5"
8
8
  description = "Agent World Protocol agent SDK for Python."
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
@@ -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
- With `heartbeat=True` a timer sends pings. An agent that actuates something should pass
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
- if self.heartbeat:
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 not None and loop.time() - last >= self._heartbeat_s():
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 = (self.conn.ready.get("heartbeat_interval_ms", 5000)) / 1000
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.tick is None and self.conn.ready is not None
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(self, predicate: Callable[[Event], bool], timeout: float = 10.0) -> Event:
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
- try:
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)."""