loomcycle 0.8.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.
Files changed (30) hide show
  1. loomcycle-0.8.0/PKG-INFO +256 -0
  2. loomcycle-0.8.0/README.md +221 -0
  3. loomcycle-0.8.0/loomcycle/__init__.py +112 -0
  4. loomcycle-0.8.0/loomcycle/_generated/__init__.py +0 -0
  5. loomcycle-0.8.0/loomcycle/_generated/loomcycle_pb2.py +218 -0
  6. loomcycle-0.8.0/loomcycle/_generated/loomcycle_pb2_grpc.py +1948 -0
  7. loomcycle-0.8.0/loomcycle/client.py +1519 -0
  8. loomcycle-0.8.0/loomcycle/errors.py +157 -0
  9. loomcycle-0.8.0/loomcycle/events.py +143 -0
  10. loomcycle-0.8.0/loomcycle/py.typed +0 -0
  11. loomcycle-0.8.0/loomcycle.egg-info/PKG-INFO +256 -0
  12. loomcycle-0.8.0/loomcycle.egg-info/SOURCES.txt +28 -0
  13. loomcycle-0.8.0/loomcycle.egg-info/dependency_links.txt +1 -0
  14. loomcycle-0.8.0/loomcycle.egg-info/requires.txt +7 -0
  15. loomcycle-0.8.0/loomcycle.egg-info/top_level.txt +1 -0
  16. loomcycle-0.8.0/pyproject.toml +87 -0
  17. loomcycle-0.8.0/setup.cfg +4 -0
  18. loomcycle-0.8.0/tests/test_channels_grpc.py +191 -0
  19. loomcycle-0.8.0/tests/test_drive_stream.py +274 -0
  20. loomcycle-0.8.0/tests/test_errors.py +178 -0
  21. loomcycle-0.8.0/tests/test_event_host_widening.py +45 -0
  22. loomcycle-0.8.0/tests/test_events.py +89 -0
  23. loomcycle-0.8.0/tests/test_helpers.py +103 -0
  24. loomcycle-0.8.0/tests/test_hooks.py +219 -0
  25. loomcycle-0.8.0/tests/test_pause_snapshot_client.py +267 -0
  26. loomcycle-0.8.0/tests/test_pause_snapshot_errors.py +149 -0
  27. loomcycle-0.8.0/tests/test_run_mutation_grpc.py +164 -0
  28. loomcycle-0.8.0/tests/test_run_request_fields.py +123 -0
  29. loomcycle-0.8.0/tests/test_substrate_client.py +147 -0
  30. loomcycle-0.8.0/tests/test_substrate_defs_grpc.py +81 -0
@@ -0,0 +1,256 @@
1
+ Metadata-Version: 2.4
2
+ Name: loomcycle
3
+ Version: 0.8.0
4
+ Summary: Async Python client for loomcycle's gRPC API — full 39-RPC parity (run streaming, batch fan-out, compaction, pause/resume/snapshot, substrate defs, channels)
5
+ Author: Dennis Gubsky
6
+ License: Apache-2.0
7
+ Project-URL: Homepage, https://github.com/denn-gubsky/loomcycle
8
+ Project-URL: Repository, https://github.com/denn-gubsky/loomcycle
9
+ Project-URL: Source, https://github.com/denn-gubsky/loomcycle/tree/main/adapters/python
10
+ Project-URL: Documentation, https://github.com/denn-gubsky/loomcycle/tree/main/adapters/python#readme
11
+ Project-URL: Changelog, https://github.com/denn-gubsky/loomcycle/releases
12
+ Project-URL: Issues, https://github.com/denn-gubsky/loomcycle/issues
13
+ Keywords: llm,agent,loomcycle,grpc,async
14
+ Classifier: Development Status :: 4 - Beta
15
+ Classifier: License :: OSI Approved :: Apache Software License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Software Development :: Libraries
23
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
24
+ Classifier: Framework :: AsyncIO
25
+ Classifier: Typing :: Typed
26
+ Classifier: Operating System :: OS Independent
27
+ Requires-Python: >=3.9
28
+ Description-Content-Type: text/markdown
29
+ Requires-Dist: grpcio>=1.80.0
30
+ Requires-Dist: protobuf<7.0.0,>=5.0.0
31
+ Provides-Extra: dev
32
+ Requires-Dist: grpcio-tools>=1.80.0; extra == "dev"
33
+ Requires-Dist: pytest>=7.4; extra == "dev"
34
+ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
35
+
36
+ # loomcycle — async Python client
37
+
38
+ `loomcycle` is the async Python client for [loomcycle][1]'s gRPC API
39
+ (introduced in v0.5.5). As of **v0.8.0 it covers all 39 gRPC RPCs** —
40
+ run streaming + continuation, batch fan-out, run compaction, agent
41
+ metadata + transcript, pause / resume / state, the snapshot lifecycle,
42
+ the resolver probe, the full substrate-def family, channel publish /
43
+ subscribe / peek / ack / await / broadcast, and run-state streaming —
44
+ through an ergonomic `LoomcycleClient` class with no need to import
45
+ generated protobuf types in your application code.
46
+
47
+ [1]: https://github.com/denn-gubsky/loomcycle
48
+
49
+ ## Status
50
+
51
+ - Wraps loomcycle's gRPC server (`LOOMCYCLE_GRPC_ADDR`).
52
+ - Async-only (`grpc.aio`). Python 3.9+.
53
+ - **Full parity with the gRPC service surface** (39 RPCs).
54
+ - The TypeScript adapter (`adapters/ts/`) additionally exposes
55
+ **HTTP-only** operations that have no gRPC RPC — memory-entry admin,
56
+ interruptions, library enumeration, the LLM gateway, and
57
+ whoami / list-users. Those are not reachable over gRPC and so are not
58
+ in this client; use the HTTP+SSE surface for them.
59
+ - Production tag: `0.8.0` (39-RPC parity, ships alongside the
60
+ loomcycle v0.34.x line).
61
+
62
+ ## Install
63
+
64
+ ```bash
65
+ pip install loomcycle
66
+ ```
67
+
68
+ ## Quick start
69
+
70
+ ```python
71
+ import asyncio
72
+ from loomcycle import LoomcycleClient
73
+
74
+ async def main():
75
+ async with LoomcycleClient(
76
+ target="127.0.0.1:8788",
77
+ auth_token="<LOOMCYCLE_AUTH_TOKEN>",
78
+ ) as client:
79
+ async for ev in client.run_streaming(
80
+ agent="default",
81
+ segments=[
82
+ {
83
+ "role": "user",
84
+ "content": [
85
+ {"type": "trusted-text", "text": "Summarize loomcycle in one sentence."}
86
+ ],
87
+ }
88
+ ],
89
+ ):
90
+ if ev.type == "text":
91
+ print(ev.text, end="", flush=True)
92
+ elif ev.type == "tool_use":
93
+ print(f"\n[tool_use: {ev.tool_use.name}]", flush=True)
94
+ elif ev.type == "done":
95
+ print(f"\n[done: {ev.stop_reason}]")
96
+
97
+ asyncio.run(main())
98
+ ```
99
+
100
+ ## Capturing the run handle
101
+
102
+ The gRPC server's first two stream frames are synthetic
103
+ registration frames — `LoomcycleClient` swallows them and exposes
104
+ the captured IDs via an `on_handle` callback:
105
+
106
+ ```python
107
+ from loomcycle import LoomcycleClient, RunHandle
108
+
109
+ async def main():
110
+ handle: RunHandle | None = None
111
+
112
+ def capture(h: RunHandle):
113
+ nonlocal handle
114
+ handle = h
115
+ print(f"agent_id={h.agent_id} session_id={h.session_id} run_id={h.run_id}")
116
+
117
+ async with LoomcycleClient(target="127.0.0.1:8788") as client:
118
+ async for ev in client.run_streaming(
119
+ agent="default",
120
+ segments=[...],
121
+ on_handle=capture,
122
+ ):
123
+ ...
124
+
125
+ # Use handle.session_id later to continue or read transcript.
126
+ ```
127
+
128
+ ## API
129
+
130
+ All methods are coroutine methods on `LoomcycleClient`.
131
+
132
+ | Method | Returns | Notes |
133
+ |---|---|---|
134
+ | `run_streaming(agent, segments, ...)` | `AsyncIterator[AgentEvent]` | Server-streams provider events for a fresh run. |
135
+ | `continue_session(session_id, segments, ...)` | `AsyncIterator[AgentEvent]` | Continues an existing session. |
136
+ | `get_agent(agent_id)` | `dict` | One agent's status + usage. |
137
+ | `cancel_agent(agent_id, reason="")` | `int` | Returns count of agents cancelled (cascades to children). |
138
+ | `list_user_agents(user_id, status="")` | `list[dict]` | Filters: `running`, `completed`, `failed`, `cancelled`. |
139
+ | `get_transcript(session_id)` | `list[dict]` | Persisted event log; `payload` is raw JSON bytes. |
140
+ | `health()` | `dict` | Liveness + build info. Unauthenticated. |
141
+ | `register_hook(owner, name, phase, callback_url, ...)` | `dict` | Pre/PostTool webhook registration. Returns `{"id": ...}`. |
142
+ | `list_hooks()` | `list[dict]` | Every registered hook (in-memory only). |
143
+ | `delete_hook(hook_id)` | `bool` | Idempotent on missing id is NOT supported — raises `HookNotFoundError`. |
144
+ | `close()` | `None` | Idempotent. Use `async with` to do this automatically. |
145
+ | `pause_runtime(timeout_ms=0)` | `dict` | v0.8.18 — quiesce the runtime. Returns `{status, duration_ms, force_cancelled_count, paused_runs_count, warnings}`. |
146
+ | `resume_runtime()` | `dict` | v0.8.18 — release the quiesce. Returns `{status, resumed_run_count, warnings}`. |
147
+ | `get_runtime_state()` | `dict` | v0.8.18 — current state. Returns `{status, paused_at, paused_run_count, snapshots_count}`. |
148
+ | `create_snapshot(description="", include_history=False, since_ts=None, max_bytes=0)` | `dict` | v0.8.18 — capture running-state JSON envelope. |
149
+ | `list_snapshots()` | `list[dict]` | v0.8.18 — metadata only; up to 200 most-recent. |
150
+ | `get_snapshot(snapshot_id)` | `dict` | v0.8.18 — full envelope including `json_content` bytes. |
151
+ | `export_snapshot(snapshot_id)` | `dict` | v0.8.18 — canonical bytes via `raw_json` for streaming consumers. |
152
+ | `restore_snapshot(snapshot_id=..., raw_json=..., include_history=False)` | `dict` | v0.8.18 — exactly one of `snapshot_id` / `raw_json`. Per-section counters returned. |
153
+ | `delete_snapshot(snapshot_id)` | `bool` | v0.8.18 — idempotent; returns True. |
154
+ | `spawn_run_batch(spawns, mode="join", timeout_ms=0)` | `dict` | v0.8.0 — spawn up to 32 runs concurrently (RFC Y); index-aligned `{spawned, results}`, per-child failures in-envelope. |
155
+ | `compact_run(run_id, reason="")` | `dict` | v0.8.0 — summarize a parked run's context. `{run_id, compacted, before_tokens, after_tokens, applied}`. |
156
+ | `resolve_probe()` | `dict` | v0.8.0 — resolver provider/model availability matrix. |
157
+ | `agent_def(input)` / `skill_def(input)` | `dict` | Substrate AgentDef / SkillDef tool; op-discriminated body. |
158
+ | `mcp_server_def` / `schedule_def` / `a2a_server_card_def` / `a2a_agent_def` / `webhook_def` / `memory_backend_def` / `operator_token_def` `(input)` | `dict` | v0.8.0 — the rest of the substrate-def family; same shape + `SubstrateToolRefusedError` contract. |
159
+ | `list_channels()` | `list[dict]` | v0.8.0 — declared + runtime channels with aggregate stats. |
160
+ | `publish_channel(channel, payload, scope="global", scope_id="", deliver_at="")` | `dict` | v0.8.0 — publish raw-JSON `payload` (bytes); `deliver_at` defers. |
161
+ | `subscribe_channel(channel, ...)` / `peek_channel(channel, ...)` | `dict` | v0.8.0 — long-poll / non-destructive read; `{messages, next_cursor?}`. |
162
+ | `ack_channel(channel, cursor, ...)` | `bool` | v0.8.0 — commit a channel cursor. |
163
+ | `await_channels(channels, mode="any", n=0, ...)` | `dict` | v0.8.0 — fan-in across channels (any / all / at_least). |
164
+ | `broadcast_channels(channels, payload, ...)` | `dict` | v0.8.0 — fan-out one payload to N channels. |
165
+ | `stream_user_run_states(user_id, statuses=None, agent="")` | `AsyncIterator[dict]` | v0.8.0 — stream a user's run-state transitions. |
166
+
167
+ `run_streaming` / `continue_session` / each `spawn_run_batch` child also accept
168
+ per-run `sampling` and `compaction` dict overrides (v0.8.0); an explicit
169
+ `temperature: 0.0` is preserved as deterministic.
170
+
171
+ ## Errors
172
+
173
+ Every method translates gRPC error codes to typed Python exceptions:
174
+
175
+ | gRPC code | Exception |
176
+ |---|---|
177
+ | `NOT_FOUND` (with session in msg) | `SessionNotFoundError` |
178
+ | `NOT_FOUND` (with hook in msg) | `HookNotFoundError` |
179
+ | `NOT_FOUND` (otherwise — agent ctx) | `AgentNotFoundError` |
180
+ | `FAILED_PRECONDITION` (session busy) | `SessionBusyError` |
181
+ | `FAILED_PRECONDITION` (other) | `LoomcycleError` |
182
+ | `ALREADY_EXISTS` | `AgentIDInUseError` |
183
+ | `RESOURCE_EXHAUSTED` (snapshot) | `SnapshotTooLargeError` (v0.8.18) |
184
+ | `RESOURCE_EXHAUSTED` (other) | `BackpressureError` |
185
+ | `UNAUTHENTICATED` | `AuthError` |
186
+ | `UNAVAILABLE` (pause not configured) | `PauseNotConfiguredError` (v0.8.18, subclass of UnavailableError) |
187
+ | `UNAVAILABLE` (other) | `UnavailableError` |
188
+ | `NOT_FOUND` (with snapshot ctx) | `SnapshotNotFoundError` (v0.8.18) |
189
+ | `FAILED_PRECONDITION` (already pausing) | `AlreadyPausingError` (v0.8.18) |
190
+ | `FAILED_PRECONDITION` (not paused) | `NotPausedError` (v0.8.18) |
191
+ | `FAILED_PRECONDITION` (snapshot version) | `SnapshotVersionError` (v0.8.18) |
192
+ | `INVALID_ARGUMENT` / `INTERNAL` / other | `LoomcycleError` |
193
+
194
+ All exceptions inherit from `LoomcycleError` and preserve the
195
+ original `grpc.StatusCode` on `.code` for log correlation:
196
+
197
+ ```python
198
+ from loomcycle import BackpressureError
199
+
200
+ try:
201
+ async for ev in client.run_streaming(...): ...
202
+ except BackpressureError as e:
203
+ log.warning("loomcycle backpressure (code=%s): %s", e.code, e.message)
204
+ ```
205
+
206
+ ## Allowed-hosts semantics
207
+
208
+ `allowed_hosts` mirrors the HTTP API's narrowing semantics:
209
+
210
+ | Value | Effect |
211
+ |---|---|
212
+ | `None` (default) | No narrowing; the operator's static allowlist is the floor. |
213
+ | `[]` | Deny-all; the agent gets no network access. |
214
+ | `["foo.com"]` | Intersection with the operator's static list. |
215
+
216
+ This is enforced server-side in `internal/tools/builtin/narrowing.go`;
217
+ `allowed_hosts` is a trust boundary — it must come from your
218
+ application code, never from a model.
219
+
220
+ ## Development
221
+
222
+ ```bash
223
+ # One-time setup:
224
+ python3 -m venv adapters/python/.venv
225
+ adapters/python/.venv/bin/pip install -e adapters/python[dev]
226
+
227
+ # Run tests (offline):
228
+ make python-test
229
+
230
+ # Regenerate stubs after editing proto/loomcycle.proto:
231
+ make python-proto
232
+ ```
233
+
234
+ The package commits its generated `loomcycle_pb2.py` /
235
+ `loomcycle_pb2_grpc.py` so end users don't need a working `protoc`
236
+ to install. Re-run `make python-proto` whenever the proto changes.
237
+
238
+ ## Live integration test
239
+
240
+ To run the example end-to-end against a local loomcycle:
241
+
242
+ ```bash
243
+ # In one shell — start loomcycle with gRPC enabled:
244
+ LOOMCYCLE_GRPC_ADDR=127.0.0.1:8788 \
245
+ LOOMCYCLE_AUTH_TOKEN=devtoken \
246
+ ./bin/loomcycle --config loomcycle.yaml
247
+
248
+ # In another shell — run the example:
249
+ LOOMCYCLE_GRPC_ADDR=127.0.0.1:8788 \
250
+ LOOMCYCLE_AUTH_TOKEN=devtoken \
251
+ adapters/python/.venv/bin/python examples/python-cli/main.py
252
+ ```
253
+
254
+ ## License
255
+
256
+ Apache-2.0. Same as loomcycle.
@@ -0,0 +1,221 @@
1
+ # loomcycle — async Python client
2
+
3
+ `loomcycle` is the async Python client for [loomcycle][1]'s gRPC API
4
+ (introduced in v0.5.5). As of **v0.8.0 it covers all 39 gRPC RPCs** —
5
+ run streaming + continuation, batch fan-out, run compaction, agent
6
+ metadata + transcript, pause / resume / state, the snapshot lifecycle,
7
+ the resolver probe, the full substrate-def family, channel publish /
8
+ subscribe / peek / ack / await / broadcast, and run-state streaming —
9
+ through an ergonomic `LoomcycleClient` class with no need to import
10
+ generated protobuf types in your application code.
11
+
12
+ [1]: https://github.com/denn-gubsky/loomcycle
13
+
14
+ ## Status
15
+
16
+ - Wraps loomcycle's gRPC server (`LOOMCYCLE_GRPC_ADDR`).
17
+ - Async-only (`grpc.aio`). Python 3.9+.
18
+ - **Full parity with the gRPC service surface** (39 RPCs).
19
+ - The TypeScript adapter (`adapters/ts/`) additionally exposes
20
+ **HTTP-only** operations that have no gRPC RPC — memory-entry admin,
21
+ interruptions, library enumeration, the LLM gateway, and
22
+ whoami / list-users. Those are not reachable over gRPC and so are not
23
+ in this client; use the HTTP+SSE surface for them.
24
+ - Production tag: `0.8.0` (39-RPC parity, ships alongside the
25
+ loomcycle v0.34.x line).
26
+
27
+ ## Install
28
+
29
+ ```bash
30
+ pip install loomcycle
31
+ ```
32
+
33
+ ## Quick start
34
+
35
+ ```python
36
+ import asyncio
37
+ from loomcycle import LoomcycleClient
38
+
39
+ async def main():
40
+ async with LoomcycleClient(
41
+ target="127.0.0.1:8788",
42
+ auth_token="<LOOMCYCLE_AUTH_TOKEN>",
43
+ ) as client:
44
+ async for ev in client.run_streaming(
45
+ agent="default",
46
+ segments=[
47
+ {
48
+ "role": "user",
49
+ "content": [
50
+ {"type": "trusted-text", "text": "Summarize loomcycle in one sentence."}
51
+ ],
52
+ }
53
+ ],
54
+ ):
55
+ if ev.type == "text":
56
+ print(ev.text, end="", flush=True)
57
+ elif ev.type == "tool_use":
58
+ print(f"\n[tool_use: {ev.tool_use.name}]", flush=True)
59
+ elif ev.type == "done":
60
+ print(f"\n[done: {ev.stop_reason}]")
61
+
62
+ asyncio.run(main())
63
+ ```
64
+
65
+ ## Capturing the run handle
66
+
67
+ The gRPC server's first two stream frames are synthetic
68
+ registration frames — `LoomcycleClient` swallows them and exposes
69
+ the captured IDs via an `on_handle` callback:
70
+
71
+ ```python
72
+ from loomcycle import LoomcycleClient, RunHandle
73
+
74
+ async def main():
75
+ handle: RunHandle | None = None
76
+
77
+ def capture(h: RunHandle):
78
+ nonlocal handle
79
+ handle = h
80
+ print(f"agent_id={h.agent_id} session_id={h.session_id} run_id={h.run_id}")
81
+
82
+ async with LoomcycleClient(target="127.0.0.1:8788") as client:
83
+ async for ev in client.run_streaming(
84
+ agent="default",
85
+ segments=[...],
86
+ on_handle=capture,
87
+ ):
88
+ ...
89
+
90
+ # Use handle.session_id later to continue or read transcript.
91
+ ```
92
+
93
+ ## API
94
+
95
+ All methods are coroutine methods on `LoomcycleClient`.
96
+
97
+ | Method | Returns | Notes |
98
+ |---|---|---|
99
+ | `run_streaming(agent, segments, ...)` | `AsyncIterator[AgentEvent]` | Server-streams provider events for a fresh run. |
100
+ | `continue_session(session_id, segments, ...)` | `AsyncIterator[AgentEvent]` | Continues an existing session. |
101
+ | `get_agent(agent_id)` | `dict` | One agent's status + usage. |
102
+ | `cancel_agent(agent_id, reason="")` | `int` | Returns count of agents cancelled (cascades to children). |
103
+ | `list_user_agents(user_id, status="")` | `list[dict]` | Filters: `running`, `completed`, `failed`, `cancelled`. |
104
+ | `get_transcript(session_id)` | `list[dict]` | Persisted event log; `payload` is raw JSON bytes. |
105
+ | `health()` | `dict` | Liveness + build info. Unauthenticated. |
106
+ | `register_hook(owner, name, phase, callback_url, ...)` | `dict` | Pre/PostTool webhook registration. Returns `{"id": ...}`. |
107
+ | `list_hooks()` | `list[dict]` | Every registered hook (in-memory only). |
108
+ | `delete_hook(hook_id)` | `bool` | Idempotent on missing id is NOT supported — raises `HookNotFoundError`. |
109
+ | `close()` | `None` | Idempotent. Use `async with` to do this automatically. |
110
+ | `pause_runtime(timeout_ms=0)` | `dict` | v0.8.18 — quiesce the runtime. Returns `{status, duration_ms, force_cancelled_count, paused_runs_count, warnings}`. |
111
+ | `resume_runtime()` | `dict` | v0.8.18 — release the quiesce. Returns `{status, resumed_run_count, warnings}`. |
112
+ | `get_runtime_state()` | `dict` | v0.8.18 — current state. Returns `{status, paused_at, paused_run_count, snapshots_count}`. |
113
+ | `create_snapshot(description="", include_history=False, since_ts=None, max_bytes=0)` | `dict` | v0.8.18 — capture running-state JSON envelope. |
114
+ | `list_snapshots()` | `list[dict]` | v0.8.18 — metadata only; up to 200 most-recent. |
115
+ | `get_snapshot(snapshot_id)` | `dict` | v0.8.18 — full envelope including `json_content` bytes. |
116
+ | `export_snapshot(snapshot_id)` | `dict` | v0.8.18 — canonical bytes via `raw_json` for streaming consumers. |
117
+ | `restore_snapshot(snapshot_id=..., raw_json=..., include_history=False)` | `dict` | v0.8.18 — exactly one of `snapshot_id` / `raw_json`. Per-section counters returned. |
118
+ | `delete_snapshot(snapshot_id)` | `bool` | v0.8.18 — idempotent; returns True. |
119
+ | `spawn_run_batch(spawns, mode="join", timeout_ms=0)` | `dict` | v0.8.0 — spawn up to 32 runs concurrently (RFC Y); index-aligned `{spawned, results}`, per-child failures in-envelope. |
120
+ | `compact_run(run_id, reason="")` | `dict` | v0.8.0 — summarize a parked run's context. `{run_id, compacted, before_tokens, after_tokens, applied}`. |
121
+ | `resolve_probe()` | `dict` | v0.8.0 — resolver provider/model availability matrix. |
122
+ | `agent_def(input)` / `skill_def(input)` | `dict` | Substrate AgentDef / SkillDef tool; op-discriminated body. |
123
+ | `mcp_server_def` / `schedule_def` / `a2a_server_card_def` / `a2a_agent_def` / `webhook_def` / `memory_backend_def` / `operator_token_def` `(input)` | `dict` | v0.8.0 — the rest of the substrate-def family; same shape + `SubstrateToolRefusedError` contract. |
124
+ | `list_channels()` | `list[dict]` | v0.8.0 — declared + runtime channels with aggregate stats. |
125
+ | `publish_channel(channel, payload, scope="global", scope_id="", deliver_at="")` | `dict` | v0.8.0 — publish raw-JSON `payload` (bytes); `deliver_at` defers. |
126
+ | `subscribe_channel(channel, ...)` / `peek_channel(channel, ...)` | `dict` | v0.8.0 — long-poll / non-destructive read; `{messages, next_cursor?}`. |
127
+ | `ack_channel(channel, cursor, ...)` | `bool` | v0.8.0 — commit a channel cursor. |
128
+ | `await_channels(channels, mode="any", n=0, ...)` | `dict` | v0.8.0 — fan-in across channels (any / all / at_least). |
129
+ | `broadcast_channels(channels, payload, ...)` | `dict` | v0.8.0 — fan-out one payload to N channels. |
130
+ | `stream_user_run_states(user_id, statuses=None, agent="")` | `AsyncIterator[dict]` | v0.8.0 — stream a user's run-state transitions. |
131
+
132
+ `run_streaming` / `continue_session` / each `spawn_run_batch` child also accept
133
+ per-run `sampling` and `compaction` dict overrides (v0.8.0); an explicit
134
+ `temperature: 0.0` is preserved as deterministic.
135
+
136
+ ## Errors
137
+
138
+ Every method translates gRPC error codes to typed Python exceptions:
139
+
140
+ | gRPC code | Exception |
141
+ |---|---|
142
+ | `NOT_FOUND` (with session in msg) | `SessionNotFoundError` |
143
+ | `NOT_FOUND` (with hook in msg) | `HookNotFoundError` |
144
+ | `NOT_FOUND` (otherwise — agent ctx) | `AgentNotFoundError` |
145
+ | `FAILED_PRECONDITION` (session busy) | `SessionBusyError` |
146
+ | `FAILED_PRECONDITION` (other) | `LoomcycleError` |
147
+ | `ALREADY_EXISTS` | `AgentIDInUseError` |
148
+ | `RESOURCE_EXHAUSTED` (snapshot) | `SnapshotTooLargeError` (v0.8.18) |
149
+ | `RESOURCE_EXHAUSTED` (other) | `BackpressureError` |
150
+ | `UNAUTHENTICATED` | `AuthError` |
151
+ | `UNAVAILABLE` (pause not configured) | `PauseNotConfiguredError` (v0.8.18, subclass of UnavailableError) |
152
+ | `UNAVAILABLE` (other) | `UnavailableError` |
153
+ | `NOT_FOUND` (with snapshot ctx) | `SnapshotNotFoundError` (v0.8.18) |
154
+ | `FAILED_PRECONDITION` (already pausing) | `AlreadyPausingError` (v0.8.18) |
155
+ | `FAILED_PRECONDITION` (not paused) | `NotPausedError` (v0.8.18) |
156
+ | `FAILED_PRECONDITION` (snapshot version) | `SnapshotVersionError` (v0.8.18) |
157
+ | `INVALID_ARGUMENT` / `INTERNAL` / other | `LoomcycleError` |
158
+
159
+ All exceptions inherit from `LoomcycleError` and preserve the
160
+ original `grpc.StatusCode` on `.code` for log correlation:
161
+
162
+ ```python
163
+ from loomcycle import BackpressureError
164
+
165
+ try:
166
+ async for ev in client.run_streaming(...): ...
167
+ except BackpressureError as e:
168
+ log.warning("loomcycle backpressure (code=%s): %s", e.code, e.message)
169
+ ```
170
+
171
+ ## Allowed-hosts semantics
172
+
173
+ `allowed_hosts` mirrors the HTTP API's narrowing semantics:
174
+
175
+ | Value | Effect |
176
+ |---|---|
177
+ | `None` (default) | No narrowing; the operator's static allowlist is the floor. |
178
+ | `[]` | Deny-all; the agent gets no network access. |
179
+ | `["foo.com"]` | Intersection with the operator's static list. |
180
+
181
+ This is enforced server-side in `internal/tools/builtin/narrowing.go`;
182
+ `allowed_hosts` is a trust boundary — it must come from your
183
+ application code, never from a model.
184
+
185
+ ## Development
186
+
187
+ ```bash
188
+ # One-time setup:
189
+ python3 -m venv adapters/python/.venv
190
+ adapters/python/.venv/bin/pip install -e adapters/python[dev]
191
+
192
+ # Run tests (offline):
193
+ make python-test
194
+
195
+ # Regenerate stubs after editing proto/loomcycle.proto:
196
+ make python-proto
197
+ ```
198
+
199
+ The package commits its generated `loomcycle_pb2.py` /
200
+ `loomcycle_pb2_grpc.py` so end users don't need a working `protoc`
201
+ to install. Re-run `make python-proto` whenever the proto changes.
202
+
203
+ ## Live integration test
204
+
205
+ To run the example end-to-end against a local loomcycle:
206
+
207
+ ```bash
208
+ # In one shell — start loomcycle with gRPC enabled:
209
+ LOOMCYCLE_GRPC_ADDR=127.0.0.1:8788 \
210
+ LOOMCYCLE_AUTH_TOKEN=devtoken \
211
+ ./bin/loomcycle --config loomcycle.yaml
212
+
213
+ # In another shell — run the example:
214
+ LOOMCYCLE_GRPC_ADDR=127.0.0.1:8788 \
215
+ LOOMCYCLE_AUTH_TOKEN=devtoken \
216
+ adapters/python/.venv/bin/python examples/python-cli/main.py
217
+ ```
218
+
219
+ ## License
220
+
221
+ Apache-2.0. Same as loomcycle.
@@ -0,0 +1,112 @@
1
+ """Async Python client for loomcycle's gRPC API.
2
+
3
+ Quick start:
4
+
5
+ import asyncio
6
+ from loomcycle import LoomcycleClient
7
+
8
+ async def main():
9
+ client = LoomcycleClient(target="127.0.0.1:8788", auth_token="...")
10
+ try:
11
+ async for ev in client.run_streaming(
12
+ agent="default",
13
+ segments=[{"role": "user", "content": [
14
+ {"type": "trusted-text", "text": "Hello, world."}
15
+ ]}],
16
+ ):
17
+ if ev.type == "text":
18
+ print(ev.text, end="", flush=True)
19
+ finally:
20
+ await client.close()
21
+
22
+ asyncio.run(main())
23
+
24
+ The client surface mirrors the gRPC service in proto/loomcycle.proto:
25
+
26
+ run_streaming(...) — server-stream events from a fresh run
27
+ continue_session(...) — server-stream events from a continuation
28
+ spawn_run_batch(...) — spawn up to 32 runs concurrently (RFC Y)
29
+ compact_run(...) — summarize a parked run's context
30
+ get_agent(...) — read one agent's status + usage
31
+ cancel_agent(...) — cancel a live agent (cascades to children)
32
+ list_user_agents(...) — list a user's recent runs
33
+ stream_user_run_states(...) — stream a user's run-state transitions
34
+ get_transcript(...) — read the persisted event log for a session
35
+ resolve_probe() — resolver provider/model availability matrix
36
+ health() — liveness probe
37
+
38
+ As of v0.8.0 the client covers all 39 gRPC RPCs: the substrate-def family
39
+ (agent_def / skill_def / mcp_server_def / schedule_def / a2a_server_card_def /
40
+ a2a_agent_def / webhook_def / memory_backend_def / operator_token_def), the
41
+ channel ops (list_channels / publish_channel / subscribe_channel / peek_channel /
42
+ ack_channel / await_channels / broadcast_channels), pause/resume/state, the
43
+ snapshot lifecycle, and hook management. run_streaming / continue_session /
44
+ spawn_run_batch accept per-run ``sampling`` + ``compaction`` overrides. The
45
+ HTTP-only surface (memory-entry admin, interruptions, library enumeration, the
46
+ LLM gateway, whoami/list-users) has no gRPC RPC and is not exposed here.
47
+
48
+ All methods are async. Server-streaming methods return an
49
+ ``AsyncIterator[AgentEvent]``. The synthetic ``"session"`` and
50
+ ``"agent"`` registration frames the gRPC server emits before the
51
+ first provider event are NOT yielded to the caller — instead they're
52
+ captured into ``RunHandle`` (returned alongside the iterator when
53
+ the caller wants the IDs without re-decoding the first frames).
54
+
55
+ For environments that can't use gRPC, use HTTP+SSE through your own
56
+ ``httpx``-based client; loomcycle's HTTP+SSE surface is
57
+ documented in the project README.
58
+ """
59
+
60
+ from .client import LoomcycleClient, RunHandle
61
+ from .events import AgentEvent, ToolUse, Usage, Retry, HostWidening
62
+ from .errors import (
63
+ LoomcycleError,
64
+ AgentNotFoundError,
65
+ SessionNotFoundError,
66
+ SessionBusyError,
67
+ AgentIDInUseError,
68
+ BackpressureError,
69
+ AuthError,
70
+ UnavailableError,
71
+ HookNotFoundError,
72
+ # v0.8.18 — pause/snapshot typed errors.
73
+ PauseNotConfiguredError,
74
+ AlreadyPausingError,
75
+ NotPausedError,
76
+ SnapshotNotFoundError,
77
+ SnapshotTooLargeError,
78
+ SnapshotVersionError,
79
+ SubstrateToolRefusedError,
80
+ InvalidArgumentError,
81
+ )
82
+
83
+ __all__ = [
84
+ "LoomcycleClient",
85
+ "RunHandle",
86
+ "AgentEvent",
87
+ "ToolUse",
88
+ "Usage",
89
+ "Retry",
90
+ "HostWidening",
91
+ "LoomcycleError",
92
+ "AgentNotFoundError",
93
+ "SessionNotFoundError",
94
+ "SessionBusyError",
95
+ "AgentIDInUseError",
96
+ "BackpressureError",
97
+ "AuthError",
98
+ "UnavailableError",
99
+ "HookNotFoundError",
100
+ # v0.8.18 additions.
101
+ "PauseNotConfiguredError",
102
+ "AlreadyPausingError",
103
+ "NotPausedError",
104
+ "SnapshotNotFoundError",
105
+ "SnapshotTooLargeError",
106
+ "SnapshotVersionError",
107
+ "InvalidArgumentError",
108
+ # v0.8.22 substrate admin
109
+ "SubstrateToolRefusedError",
110
+ ]
111
+
112
+ __version__ = "0.8.0"
File without changes