Skip to main content

loomcycle — async Python client

loomcycle is the async Python client for loomcycle's gRPC API (introduced in v0.5.5). As of v1.1.1 it covers all 42 gRPC RPCs — run streaming + continuation, the RFC AI interactive session (run_input + stream_run + an interactive=True flag), batch fan-out, run compaction, agent metadata + transcript, pause / resume / state, the snapshot lifecycle, the resolver probe, the full substrate-def family (incl. RFC AH volume_def), channel publish / subscribe / peek / ack / await / broadcast, and run-state streaming — through an ergonomic LoomcycleClient class with no need to import generated protobuf types in your application code.

Status

  • Wraps loomcycle's gRPC server (LOOMCYCLE_GRPC_ADDR).
  • Async-only (grpc.aio). Python 3.9+.
  • Full parity with the gRPC service surface (42 RPCs).
  • The TypeScript adapter (adapters/ts/) additionally exposes HTTP-only operations that have no gRPC RPC — memory-entry admin, interruptions, library enumeration, the LLM gateway, and whoami / list-users. Those are not reachable over gRPC and so are not in this client; use the HTTP+SSE surface for them.
  • Production tag: 1.1.1 (42-RPC parity, version-aligned with the loomcycle v1.1.x line; ships on the python-v1.1.1 tag).

Install

pip install loomcycle

Quick start

import asyncio
from loomcycle import LoomcycleClient

async def main():
    async with LoomcycleClient(
        target="127.0.0.1:8788",
        auth_token="<LOOMCYCLE_AUTH_TOKEN>",
    ) as client:
        async for ev in client.run_streaming(
            agent="default",
            segments=[
                {
                    "role": "user",
                    "content": [
                        {"type": "trusted-text", "text": "Summarize loomcycle in one sentence."}
                    ],
                }
            ],
        ):
            if ev.type == "text":
                print(ev.text, end="", flush=True)
            elif ev.type == "tool_use":
                print(f"\n[tool_use: {ev.tool_use.name}]", flush=True)
            elif ev.type == "done":
                print(f"\n[done: {ev.stop_reason}]")

asyncio.run(main())

Capturing the run handle

The gRPC server's first two stream frames are synthetic registration frames — LoomcycleClient swallows them and exposes the captured IDs via an on_handle callback:

from loomcycle import LoomcycleClient, RunHandle

async def main():
    handle: RunHandle | None = None

    def capture(h: RunHandle):
        nonlocal handle
        handle = h
        print(f"agent_id={h.agent_id} session_id={h.session_id} run_id={h.run_id}")

    async with LoomcycleClient(target="127.0.0.1:8788") as client:
        async for ev in client.run_streaming(
            agent="default",
            segments=[...],
            on_handle=capture,
        ):
            ...

    # Use handle.session_id later to continue or read transcript.

API

All methods are coroutine methods on LoomcycleClient.

Method Returns Notes
run_streaming(agent, segments, ...) AsyncIterator[AgentEvent] Server-streams provider events for a fresh run.
continue_session(session_id, segments, ...) AsyncIterator[AgentEvent] Continues an existing session.
get_agent(agent_id) dict One agent's status + usage.
get_run(run_id) dict One run by its run id — get_agent's shape; use it when an agent id names several runs (every walk of a team is team:<name>).
cancel_agent(agent_id, reason="") int Returns count of agents cancelled (cascades to children).
list_user_agents(user_id, status="") list[dict] Filters: running, completed, failed, cancelled.
list_walk_runs(walk_id, limit=None, cursor=None) dict One page of a team walk's runs — its own run and every member, oldest first: {"agents": [...], "next_cursor": str}. Pass next_cursor back as cursor; "" on the last page. limit defaults to 100, at most 1000.
get_transcript(session_id) list[dict] Persisted event log; payload is raw JSON bytes.
health() dict Liveness + build info. Unauthenticated.
get_config() dict v1.38.0 — instance configuration: build identity, the feature matrix, and the live provider/model/search cascade with active/selected. view names the disclosure level (authenticated or admin; the HTTP surface's narrower public level does not exist over gRPC, which authenticates before dispatch).
hook_def(input) dict Reusable hook definitions (create / fork / get / list / promote / retire / verify / delete). An agent attaches hooks in its own definition.
close() None Idempotent. Use async with to do this automatically.
pause_runtime(timeout_ms=0) dict v0.8.18 — quiesce the runtime. Returns {status, duration_ms, force_cancelled_count, paused_runs_count, warnings}.
resume_runtime() dict v0.8.18 — release the quiesce. Returns {status, resumed_run_count, warnings}.
get_runtime_state() dict v0.8.18 — current state. Returns {status, paused_at, paused_run_count, snapshots_count}.
create_snapshot(description="", include_history=False, since_ts=None, max_bytes=0) dict v0.8.18 — capture running-state JSON envelope.
list_snapshots() list[dict] v0.8.18 — metadata only; up to 200 most-recent.
get_snapshot(snapshot_id) dict v0.8.18 — full envelope including json_content bytes.
export_snapshot(snapshot_id) dict v0.8.18 — canonical bytes via raw_json for streaming consumers.
restore_snapshot(snapshot_id=..., raw_json=..., include_history=False) dict v0.8.18 — exactly one of snapshot_id / raw_json. Per-section counters returned.
delete_snapshot(snapshot_id) bool v0.8.18 — idempotent; returns True.
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.
compact_run(run_id, reason="") dict v0.8.0 — summarize a parked run's context. {run_id, compacted, before_tokens, after_tokens, applied}.
resolve_probe() dict v0.8.0 — resolver provider/model availability matrix.
agent_def(input) / skill_def(input) dict Substrate AgentDef / SkillDef tool; op-discriminated body.
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.
volume_def(input) dict v0.9.0 — RFC AH dynamic filesystem-volume substrate; op-discriminated (create / get / list / delete / purge), tenant-confined, same SubstrateToolRefusedError contract.
run_input(run_id, text) dict v1.1.1 — RFC AI; steer a live interactive run. {run_id, delivered}. NotFound (AgentNotFoundError) / ResourceExhausted (BackpressureError) on a gone run / full queue.
stream_run(run_id, from_seq=0) AsyncIterator[AgentEvent] v1.1.1 — RFC AI; re-attach to a run's events (replay-then-tail). Operator turns replay as steer events (user_input.source=="replay"). Pair with interactive=True on run_streaming / continue_session.
list_channels() list[dict] v0.8.0 — declared + runtime channels with aggregate stats.
publish_channel(channel, payload, scope="global", scope_id="", deliver_at="") dict v0.8.0 — publish raw-JSON payload (bytes); deliver_at defers.
subscribe_channel(channel, ...) / peek_channel(channel, ...) dict v0.8.0 — long-poll / non-destructive read; {messages, next_cursor?}.
ack_channel(channel, cursor, ...) bool v0.8.0 — commit a channel cursor.
await_channels(channels, mode="any", n=0, ...) dict v0.8.0 — fan-in across channels (any / all / at_least).
broadcast_channels(channels, payload, ...) dict v0.8.0 — fan-out one payload to N channels.
stream_user_run_states(user_id, statuses=None, agent="", walk_id="") AsyncIterator[dict] v0.8.0 — stream a user's run-state transitions. walk_id (1.78.0) narrows it server-side to the runs one team walk spawned; each event carries parent_context (None outside a walk) with walk_id / state / state_visit (every member) and wave_id / wave_index (a starter's wave).

run_streaming / continue_session / each spawn_run_batch child also accept per-run sampling and compaction dict overrides (v0.8.0); an explicit temperature: 0.0 is preserved as deterministic.

Errors

Every method translates gRPC error codes to typed Python exceptions:

gRPC code Exception
NOT_FOUND (with session in msg) SessionNotFoundError
NOT_FOUND (with hook in msg) HookNotFoundError
NOT_FOUND (otherwise — agent ctx) AgentNotFoundError
FAILED_PRECONDITION (session busy) SessionBusyError
FAILED_PRECONDITION (other) LoomcycleError
ALREADY_EXISTS AgentIDInUseError
RESOURCE_EXHAUSTED (snapshot) SnapshotTooLargeError (v0.8.18)
RESOURCE_EXHAUSTED (other) BackpressureError
UNAUTHENTICATED AuthError
UNAVAILABLE (pause not configured) PauseNotConfiguredError (v0.8.18, subclass of UnavailableError)
UNAVAILABLE (other) UnavailableError
NOT_FOUND (with snapshot ctx) SnapshotNotFoundError (v0.8.18)
FAILED_PRECONDITION (already pausing) AlreadyPausingError (v0.8.18)
FAILED_PRECONDITION (not paused) NotPausedError (v0.8.18)
FAILED_PRECONDITION (snapshot version) SnapshotVersionError (v0.8.18)
INVALID_ARGUMENT / INTERNAL / other LoomcycleError

All exceptions inherit from LoomcycleError and preserve the original grpc.StatusCode on .code for log correlation:

from loomcycle import BackpressureError

try:
    async for ev in client.run_streaming(...): ...
except BackpressureError as e:
    log.warning("loomcycle backpressure (code=%s): %s", e.code, e.message)

Allowed-hosts semantics

allowed_hosts mirrors the HTTP API's narrowing semantics:

Value Effect
None (default) No narrowing; the operator's static allowlist is the floor.
[] Deny-all; the agent gets no network access.
["foo.com"] Intersection with the operator's static list.

This is enforced server-side in internal/tools/builtin/narrowing.go; allowed_hosts is a trust boundary — it must come from your application code, never from a model.

Development

# One-time setup:
python3 -m venv adapters/python/.venv
adapters/python/.venv/bin/pip install -e adapters/python[dev]

# Run tests (offline):
make python-test

# Regenerate stubs after editing proto/loomcycle.proto:
make python-proto

The package commits its generated loomcycle_pb2.py / loomcycle_pb2_grpc.py so end users don't need a working protoc to install. Re-run make python-proto whenever the proto changes.

Live integration test

To run the example end-to-end against a local loomcycle:

# In one shell — start loomcycle with gRPC enabled:
LOOMCYCLE_GRPC_ADDR=127.0.0.1:8788 \
LOOMCYCLE_AUTH_TOKEN=devtoken \
./bin/loomcycle --config loomcycle.yaml

# In another shell — run the example:
LOOMCYCLE_GRPC_ADDR=127.0.0.1:8788 \
LOOMCYCLE_AUTH_TOKEN=devtoken \
adapters/python/.venv/bin/python examples/python-cli/main.py

License

Apache-2.0. Same as loomcycle.

Metadata

Release files for loomcycle 1.105.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for loomcycle 1.105.0
File Size Uploaded
loomcycle-1.105.0.tar.gz 108.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for loomcycle 1.105.0
File Interpreter ABI Platform
loomcycle-1.105.0-py3-none-any.whl Python 3 none any Details

Total release size: 181.5 kB

Release files / loomcycle-1.105.0.tar.gz

Download URL loomcycle-1.105.0.tar.gz
Size 108.7 kB
Tags Source
SHA-256 checksum
How to use checksums
dc99de4ab47f6a59402bd112cbe950937259107b066b04ef765de873527730cb
BLAKE2b-256 checksum
How to use checksums
4a629c4f77eef9430e601962862b9b3985826c1374970f025affd52fb823417c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 8, 2026.

Transparency log

Release files / loomcycle-1.105.0-py3-none-any.whl

Download URL loomcycle-1.105.0-py3-none-any.whl
Size 72.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
b6862a1f1823fd21777d3fbd2ae952ca0c92c277882d866f0348cf1777895907
BLAKE2b-256 checksum
How to use checksums
5e67c1767379672b078554df8a37f67f62b716bbf843055fee4b436007afb3e8
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 8, 2026.

Transparency log
Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page