Actae Python SDK
Distributed agent coordination and Group Fork are documented in
../../docs/EXECUTION_GROUPS.md. Useclient.execution_group(id)for durable messaging, member leases,wait_for/wait_any/wait_all, acknowledgement, promotion, and counterfactual receipts.
Full-featured Python client for Actae — record, replay, query, subscribe, state snapshots, fork/experiment branches, consumer groups, wake-ups, and auth.
pip install actae-client
Requires Python 3.9+ and aiohttp. Framework adapters (LangGraph, Claude,
OpenAI Agents, …) are optional extras — see Framework Adapters.
For LLM consumption: a single-file consolidated reference is at
SDK_REFERENCE.md.
Quick Start
import asyncio
from actae_client import ActaeClient
async def main():
async with ActaeClient(
endpoint="http://localhost:8002",
api_key="sk-dev-0000000000000000000000",
) as client:
event = await client.record(
"my-channel", "tool.completed",
payload={"result": "success"},
actor="my-agent",
)
print(f"Recorded: cursor={event.cursor}")
events = await client.replay("my-channel", cursor=0)
for e in events:
print(f" [{e.cursor}] {e.event_type}")
asyncio.run(main())
api_key is required; endpoint alone is enough to derive the WebSocket URL
automatically (http:// → ws://…/ws, https:// → wss://…/ws). The
server runs everything (HTTP API, WebSocket, health, metrics) on one port:
8002 (dev mode: 8002, auto-bumping to the next free port when busy).
replay(channel, cursor=...) uses exclusive cursor semantics: events with
a per-channel cursor strictly greater than the given value are returned (pass
N - 1 for events up to and including cursor N). The same exclusive
semantics apply to the cursor argument of subscribe() and stream().
Cursors are per-channel: each channel numbers its events 1, 2, 3, …
independently (gapless), so cursor is always relative to a single channel.
Connecting to your Actae instance
You need two things: the endpoint (where Actae runs) and an API key (who you are). Where you get them depends on how you run Actae:
| Setup | Endpoint | API key |
|---|---|---|
| Actae Cloud (just subscribed) | Your instance URL from the dashboard, e.g. https://your-org.actae.app |
Create a key in the dashboard → Settings → API keys (sk-...) |
| Self-hosted, dev mode | http://localhost:8002 (printed in the startup banner) |
sk-dev-0000000000000000000000 |
| Self-hosted, docker compose | https://your-server:8002 (TLS) |
The ACTAE_API_KEY you provisioned (see docs/PRODUCTION.md) |
| Actae Cloud | The HTTPS endpoint shown in the portal | The separately generated diviga_… SDK key shown once |
Three kinds of credentials exist — here's when each is used:
- API key (
sk-...) — the primary credential. Pass it toActaeClientasapi_key; every HTTP call and the WebSocket auth use it. This is the only credential most users ever touch. - JWT (
client.auth.signup/login) — user-account tokens for the dashboard-facing auth endpoints (get_me,logout). The SDK does not hold or reuse them automatically; pass them per call. - mTLS client certificate — optional transport-level auth on top of the API key, required only if your instance enforces it. See below.
Real-time: subscribe and stream
async with ActaeClient(endpoint="http://localhost:8002", api_key="sk-...", echo_self=True) as client:
# Option A — async iteration (recommended). Blocks until an event
# arrives; this demo publishes its own (echo_self) so it terminates —
# omit `break` to keep streaming live events.
async def show():
async for event in client.stream("my-channel"):
print(f"[{event.cursor}] {event.event_type}: {event.payload}")
break
task = asyncio.create_task(show())
# Option B — callback style:
client.on_message(lambda topic, event: print(topic, event.event_type))
await client.subscribe("my-channel") # wait=True by default
await client.publish("my-channel", {"hello": "world"})
await task
Both forms block until events arrive — a live stream never "finishes".
Note: the server does not echo a publish back to the connection that published it. To see your own events live, either subscribe with one client and publish with another, or construct the client with
echo_self=True— then your own publishes are delivered to this client'son_messagecallbacks andstream()iterators too (mirrors the Go SDK'sEchoSelf).
Sync code? No problem.
The most common calls also exist in a blocking form for non-async codebases:
client = ActaeClient(endpoint="http://localhost:8002", api_key="sk-...")
event = client.record_sync("my-channel", "tool.completed", {...}, actor="me")
events = client.replay_sync("my-channel")
state = client.latest_state_sync("my-channel")
Each blocking call runs on its own per-thread background event loop, so you can mix sync and async use of the same client freely.
| Sync variant | Async counterpart |
|---|---|
record_sync(channel, type, payload, *, actor, ...) |
record |
replay_sync(channel, *, cursor?, limit?) |
replay |
query_sync(*, event_type?, ...) |
query |
get_cursor_sync(channel) |
get_cursor |
transition_sync(channel, type, payload, state, ...) |
transition |
fork_sync(source, new, at_cursor=0, ...) |
fork |
latest_state_sync(channel) |
latest_state |
save_state_sync(channel, cursor, state, ...) |
save_state |
disconnect_sync() |
disconnect — call at shutdown to release the WS + HTTP sessions |
WebSocket methods (subscribe, stream, publish, …), consumer groups,
executions, wake-ups, and AgentSession are async-only — the table shows
which async call each _sync method maps to.
Which API should I use?
Actae is an event store with a lot of surface. This table tells you where to go for each use case:
| Use case | Start here |
|---|---|
| Log what an agent did | client.events.record(channel, type, payload, actor=...) |
| Store agent context so a run can be resumed | client.state.save_state(channel, cursor, state) |
| Record an event and its resulting state atomically | client.events.transition(...) |
| Replay / query what happened | client.events.replay(...), client.events.query(...) |
| Watch events live | client.ws.stream(channel) or client.ws.subscribe(...) |
| High-level "session" wrapper (steps, forks, resume) | AgentSession — see below |
| Framework-agnostic state save/load/resume | StateManager adapter |
| A/B test by forking a channel into branches | client.channels.fork(...) |
| Prove which fork fixed a failure (state diff) | client.channels.diff_states(left, right) |
| Audit a branch (lineage + tool executions) | client.channels.decision_trail(channel) |
| Durable worker consuming a channel | client.groups.* |
| Schedule a one-off event in the future | client.wakeups.schedule_wakeup(...) |
| Coordinate idempotent tool execution across retries | client.executions.claim_execution(...) |
Every method lives on the client (client.record(...)) and on a
namespaced facade (client.events.record(...)). The facades just group
methods by concern — they are the same calls, so pick whichever reads
better in your code.
"How do I persist agent state?" — the four ways, and when
State persistence is the one place Actae offers overlapping primitives. Pick by how much structure you want:
| You want to… | Use | Notes |
|---|---|---|
| Persist raw context manually | save_state(channel, cursor, state) |
You fetch the cursor yourself (latest_cursor()) |
| Record an event + state atomically | transition(...) |
Both commit in one server-side transaction |
| Steps + auto-snapshots + fork/resume | AgentSession(state_fn=...) |
Saves state for you every N steps; state_fn may be sync or async |
| Framework-agnostic save/load/resume helpers | StateManager(actae, channel=...) |
Thin wrapper that hides cursor handling |
| LangGraph checkpoints | ActaeCheckpointSaver |
Framework-specific, see adapters below |
Rule of thumb: transition when the state belongs to a specific event,
AgentSession when you're instrumenting a multi-step agent run, and
StateManager (or raw save_state) when you want full control.
Client
ActaeClient(
*,
api_key: str,
endpoint: str = "",
ws_endpoint: str = "",
ssl_context: Optional[ssl.SSLContext] = None,
client_cert: Optional[str] = None,
client_key: Optional[str] = None,
ca_cert: Optional[str] = None,
timeout: float = 30.0,
auto_reconnect: bool = True,
echo_self: bool = False,
)
| Param | Default | Notes |
|---|---|---|
api_key |
required | API key for authentication |
endpoint |
"" |
HTTP API base URL (e.g. http://localhost:8002) |
ws_endpoint |
"" |
WebSocket URL — auto-derived from endpoint if omitted |
client_cert / client_key / ca_cert |
None |
mTLS cert files |
timeout |
30.0 |
Request/connection timeout (seconds) |
auto_reconnect |
True |
Auto-reconnect WebSocket on disconnect (resubscribes topics) |
echo_self |
False |
Deliver this client's own publishes to its on_message/stream() (Go SDK EchoSelf parity) |
Use it as an async context manager (async with) or call
await client.connect() / await client.disconnect() manually.
Connecting to an mTLS-protected instance
A production Actae instance requires every client to present a certificate signed by the instance's client CA (this is transport-level auth — the API key is checked on top of it). Two ways to supply the certs:
Zero-config (recommended). Drop the identity into the config dir once —
~/.actae/ (or ACTAE_CONFIG_DIR) with ca.crt, client.crt,
client.key — and the SDK finds it automatically for any https:// /
wss:// endpoint:
# on the server: issue and install an identity
./scripts/issue-client-cert.sh deploy-agent --install
# (for a remote client, use --bundle and extract into ~/.actae)
ActaeClient(endpoint="https://actae.example.com", api_key="sk-...") # certs auto-loaded
Explicit. Pass the paths yourself — overrides the config dir:
ActaeClient(
endpoint="https://actae.example.com",
api_key="sk-...",
ca_cert="certs/ca.crt", # trust the server
client_cert="certs/client/deploy-agent.crt", # identify yourself
client_key="certs/client/deploy-agent.key",
)
ws_endpoint derives automatically (https:// → wss://…/ws), and the
same certs are used for both HTTP and WebSocket. If the connection fails
and no certs are configured, the error message tells you exactly how to
fix it.
Namespaced API surface
| Facade | Methods |
|---|---|
client.events |
record, replay, query, transition, get_cursor, latest_cursor |
client.state |
save_state, latest_state, list_states, get_state, delete_state |
client.channels |
list_channels, fork, get_channel_metadata, list_branches, get_execution_tree, update_metadata, diff_states, decision_trail |
client.executions |
claim_execution, complete_execution, fail_execution, heartbeat_execution, cancel_execution, get_execution, list_executions, delete_execution |
client.groups |
create_group, list_groups, delete_group, join_group, claim_work, ack_work, heartbeat, group_offsets |
client.wakeups |
schedule_wakeup, list_wakeups, get_wakeup, cancel_wakeup |
client.health |
health_check, readiness_check, get_metrics_text, get_metrics_json |
client.auth |
signup, login, logout, get_me |
client.ws |
connect, disconnect, subscribe, unsubscribe, publish, stream, on_message, on_error, on_subscribed, on_disconnected, on_reconnect |
Plus blocking variants for sync code: record_sync, replay_sync,
query_sync, get_cursor_sync, transition_sync, fork_sync,
latest_state_sync, save_state_sync.
Concepts (in one paragraph)
- Channel — a named event log (e.g.
"my-agent"or"exp-v1"). All writes and replays target a channel. Channels can be forked into experiment branches. - Event — one immutable entry on a channel:
event_type+payload+actor, stamped with a gapless per-channelcursor(each channel numbers its events 1, 2, 3, … independently). - State snapshot — a versioned, cursor-aligned JSON blob attached to a
channel (
save_state→latest_state). It is how you persist "where my agent got to" so a run can resume after a crash. - Topic — what WebSocket methods call a channel.
subscribe("ch")andreplay("ch")talk about the same thing.
Methods
Events (HTTP)
Cursor semantics (applies to every
cursor-taking event method): cursors are per-channel (each channel numbers its events 1, 2, 3, … independently and gapless), and boundaries are exclusive — passN - 1to include cursorN.Event.cursoris the per-channel cursor (used by consumer groups viaack_workand state alignment);Event.channel_cursoris a deprecated alias carrying the same value.
| Method | Returns | Server |
|---|---|---|
record(channel_id, event_type, payload, *, actor, agent_id?, user_id?, metadata?, operation_id?) |
Event |
POST /api/v1/events/record |
transition(channel_id, event_type, payload, state, *, actor="system", agent_id?, user_id?, metadata?, expected_version?, expected_cursor?) |
TransitionResult (event + state_version) |
POST /api/v1/events/transition |
replay(channel_id, *, cursor?, limit?, event_type?) |
List[Event] |
GET /api/v1/events/replay/{channel_id} |
query(*, channel_ids?, event_type?, actor?, cursor_start?, cursor_end?, from_time?, to_time?, limit?, offset?) |
List[Event] |
POST /api/v1/events/query |
get_cursor(channel_id) |
Optional[int] |
GET /api/v1/events/cursor/{channel_id} |
State (HTTP — versioned snapshots)
The state family returns different shapes — here they are at a glance:
| Method | Returns | Server |
|---|---|---|
save_state(channel_id, cursor, state, *, expected_version?, expected_cursor?) |
int (new version) |
POST /api/v1/state/{channel_id} |
latest_state(channel_id) |
Optional[{"cursor": int, "state": ...}] |
GET /api/v1/state/{channel_id} |
list_states(channel_id, *, limit?, offset?) |
List[{"version": int, "cursor": int, "timestamp": str}] |
GET /api/v1/state/{channel_id}/versions |
get_state(channel_id, version) |
Optional[{"cursor": int, "version": int, "state": ...}] |
GET /api/v1/state/{channel_id}/version/{version} |
delete_state(channel_id, version) |
None |
DELETE /api/v1/state/{channel_id}/version/{version} |
save_state takes the global cursor of the event the snapshot aligns
with — fetch it from the event you just recorded (event.cursor) or
get_cursor(channel_id). If this cursor bookkeeping gets in the way, use
StateManager or AgentSession, which handle it for you.
Channels & Fork (HTTP)
| Method | Returns | Server |
|---|---|---|
list_channels() |
List[str] |
GET /api/v1/channels — ordered by most recent activity |
fork(source, new, at_cursor=0, *, display_name?, reason?, experiment_metadata?, operation_id?, manifest?, expected_version?, expected_cursor?) |
ForkReceipt (immutable) |
POST /api/v1/channels/fork |
get_fork_receipt(channel_id) |
ForkReceipt |
GET /api/v1/channels/{channel_id}/receipt |
resolve_step(channel_id, step_number) |
(owner, cursor, event_id)? |
GET /api/v1/channels/{channel_id}/steps/{step_number} |
get_channel_metadata(channel_id) |
Optional[ChannelMetadata] |
GET /api/v1/channels/{channel_id}/metadata |
list_branches(channel_id) |
List[ChannelMetadata] |
GET /api/v1/channels/{channel_id}/branches |
get_execution_tree(root_channel_id) |
BranchInfo |
GET /api/v1/channels/{root_channel_id}/tree |
update_metadata(channel_id, *, display_name?, reason?, experiment_metadata?) |
ChannelMetadata |
PUT /api/v1/channels/{channel_id}/metadata |
diff_states(left, right) |
dict (per-side divergence + honest truncation) |
GET /api/v1/channels/diff |
compare_channels(left, right) |
dict (state + manifest + tools + output + metrics) |
GET /api/v1/channels/compare |
set_outcome(channel_id, outcome, *, score?) |
dict |
PATCH /api/v1/channels/{channel_id}/outcome |
promote_channel(channel_id) |
dict |
POST /api/v1/channels/{channel_id}/promote |
delete_channel(channel_id, recursive=False) |
dict |
DELETE /api/v1/channels/{channel_id} |
create_experiment / list_experiments / get_experiment / add_experiment_member / rank_experiment |
dict |
POST/GET /api/v1/experiments... |
Boundary policy (AgentSession.fork / resume(fork_at_step=)):
boundary_mode="exact" (default) raises NoRestorableCheckpointError when no
snapshot exists at the requested step — it never silently inherits later
state. "approximate" falls back to the latest state and reports the drift
via session.resolved_boundary_cursor; "lineage_only" creates the branch
with no state copy. Fork errors: SnapshotBoundaryError,
IdempotencyConflictError, ChannelConflictError.
Consumer groups (HTTP — durable workers)
At-least-once channel consumption with leases: consumers join a group,
claim_work batches, process, ack_work to commit the offset. Crashed
workers' unacked work is redelivered after lease expiry.
| Method | Returns | Server |
|---|---|---|
create_group(group_id, channel_id, *, metadata?) |
GroupInfo |
POST /api/v1/groups |
list_groups(*, channel_id?) |
List[GroupInfo] |
GET /api/v1/groups |
delete_group(group_id) |
None |
DELETE /api/v1/groups/{group_id} |
join_group(group_id, consumer_id, *, lease_seconds=60) |
GroupOffset |
POST /api/v1/groups/{group_id}/join |
claim_work(group_id, consumer_id, *, limit=100) |
ClaimedWork (events + lease_until) |
POST /api/v1/groups/{group_id}/work |
ack_work(group_id, consumer_id, cursor) |
None |
POST /api/v1/groups/{group_id}/ack |
heartbeat(group_id, consumer_id, *, lease_seconds=60) |
None |
POST /api/v1/groups/{group_id}/heartbeat |
group_offsets(group_id) |
List[GroupOffset] |
GET /api/v1/groups/{group_id}/offsets |
A minimal durable worker (at-least-once: claim → process → ack; crashed workers' batches are redelivered after lease expiry):
async def worker(client, group_id, consumer_id):
await client.groups.join_group(group_id, consumer_id, lease_seconds=60)
while True:
work = await client.groups.claim_work(group_id, consumer_id, limit=50)
if not work.events:
await asyncio.sleep(1); continue
for event in work.events: # process in cursor order
await handle(event)
await client.groups.ack_work(group_id, consumer_id, event.channel_cursor)
await client.groups.heartbeat(group_id, consumer_id) # keep the lease
Two rules to keep straight: ack is a watermark — always ack the highest
contiguously processed event.cursor (never a higher one, or events
are skipped permanently); and heartbeat while you work if a batch takes
longer than the lease.
Idempotent tool executions (HTTP)
Coordinated tool retries: claim_execution returns claimed (you run the
tool) or replayed (a previous run's result is returned instead). Finish
with complete_execution / fail_execution, keep the lease alive with
heartbeat_execution, or cancel_execution. Actae coordinates the retry and
replays recorded results; exactly-once external effects additionally require an
idempotent or queryable downstream operation (see the failure boundary below).
| Method | Returns | Server |
|---|---|---|
claim_execution(channel_id, key_name, tool_name, params=None, *, dedup_fields?, lease_seconds?, emit_replay_event?, actor?) |
ExecutionClaim |
POST /api/v1/executions/claim |
complete_execution(execution_id, claim_token=None, result=None) |
ExecutionInfo |
POST /api/v1/executions/{id}/complete |
fail_execution(execution_id, message, *, claim_token?, error_type?, stack?) |
ExecutionInfo |
POST /api/v1/executions/{id}/fail |
heartbeat_execution(execution_id, claim_token=None, lease_seconds=None) |
ExecutionInfo |
POST /api/v1/executions/{id}/heartbeat |
cancel_execution(execution_id, claim_token=None) |
ExecutionInfo |
POST /api/v1/executions/{id}/cancel |
get_execution(execution_id) |
ExecutionInfo |
GET /api/v1/executions/{id} |
list_executions(channel_id, limit=50) |
List[ExecutionInfo] |
GET /api/v1/executions |
delete_execution(execution_id) |
bool |
DELETE /api/v1/executions/{id} |
Minimal coordinated-retry pattern — the same key retried by any caller returns the recorded result instead of running the tool again:
claim = await client.executions.claim_execution(
"orders", "charge-42", "charge_customer", params={"amount": 9.99},
)
if claim.status == "replayed":
return claim.result # already ran — return it
if claim.status == "in_progress":
raise RetryLater() # someone else owns it
try:
result = await run_tool(claim.execution)
await client.executions.complete_execution(
claim.execution.id, claim_token=claim.claim_token, result=result)
except Exception as e:
await client.executions.fail_execution(
claim.execution.id, str(e), claim_token=claim.claim_token)
The failure boundary. Actae coordinates the retry: it deduplicates completed executions, fences stale owners with claim tokens, and replays recorded results. It does not make the external operation itself atomic:
claim
↓
run external operation
↓
persist completion
If the external operation succeeds but the worker dies before Actae records completion, the lease expires and a later worker may run the operation again. Exactly-once external effects additionally require the downstream operation to be idempotent or queryable — for example by passing the same downstream idempotency key, or by checking the upstream state before acting. Actae guarantees coordination and recorded-result replay; it cannot close the gap between an external call and the commit that records it.
Wake-ups (HTTP — persisted scheduling)
run_at accepts either an RFC 3339 string ("2026-08-05T12:00:00Z") or a
timezone-aware datetime (naive datetimes raise ValueError).
| Method | Returns | Server |
|---|---|---|
schedule_wakeup(channel_id, run_at, *, payload?) |
Wakeup |
POST /api/v1/scheduler/wakeups |
list_wakeups(*, channel_id?, status?) |
List[Wakeup] |
GET /api/v1/scheduler/wakeups |
get_wakeup(wakeup_id) |
Optional[Wakeup] |
GET /api/v1/scheduler/wakeups/{id} |
cancel_wakeup(wakeup_id) |
bool |
DELETE /api/v1/scheduler/wakeups/{id} |
Health & readiness (HTTP)
| Method | Returns | Server |
|---|---|---|
health_check() |
HealthStatus |
GET /healthz |
readiness_check() |
ReadinessResult |
GET /readyz |
get_metrics_text() |
str (Prometheus format) |
GET /metrics |
get_metrics_json() |
MetricsSnapshot |
GET /metrics.json |
Auth (HTTP — user management)
| Method | Returns | Server |
|---|---|---|
signup(email, password, name?) |
AuthResult (user + JWT) |
POST /api/v1/auth/signup |
login(email, password) |
AuthResult (user + JWT) |
POST /api/v1/auth/login |
logout(jwt_token) |
None |
POST /api/v1/auth/logout |
get_me(jwt_token) |
UserInfo |
GET /api/v1/auth/me |
WebSocket
| Method | Returns | Notes |
|---|---|---|
connect() |
None |
Open WebSocket connection (auto on async with) |
disconnect() |
None |
Close WebSocket connection |
subscribe(topic, *, cursor?, wait=True) |
None |
Subscribe; cursor replays events with a cursor strictly greater than it (exclusive, like replay); wait=True (default) confirms before returning |
unsubscribe(topic) |
None |
Unsubscribe from channel |
publish(topic, payload, *, operation_id?) |
Event |
Broadcast event over WS, returns persisted event via Ack; operation_id makes retries idempotent |
stream(topic, *, cursor?) |
async iterator[Event] |
async for event in client.stream("ch"): ... |
on_message(callback) |
None |
callback(topic, event) — multiple callbacks allowed |
on_error(callback) |
None |
callback(message) |
on_subscribed(callback) |
None |
callback(topic, cursor) |
on_disconnected(callback) |
None |
callback() — fires before auto-reconnect |
on_reconnect(callback) |
None |
callback() — fires after resubscribe |
On reconnect the client resumes each topic at the last event it received, so
no events are re-delivered after a drop. If you ever need belt-and-braces
single-processing on top, deduplicate by event.id (stable UUID per event).
Agents & Sessions
AgentSession wraps a channel with a step → cursor mapping so you can fork
at a step number instead of a raw cursor, and auto-saves state snapshots
every snapshot_interval steps via state_fn (state_fn may be a sync or
an async function):
from actae_client import ActaeClient
from actae_client.session import AgentSession
actae = ActaeClient(endpoint="http://localhost:8002", api_key="sk-...")
async def main():
async with AgentSession(
actae,
name="exp-v1", # used as the channel ID
display_name="Temperature Search", # human-readable name in dashboard
state_fn=lambda: agent.state,
) as session:
result = await session.step("inference", input="prompt", output="response")
asyncio.run(main())
Lifecycle is created → started → stepping → completed (or crashed).
Three entry points:
| You want to… | Use |
|---|---|
| Start recording a fresh run | AgentSession(actae, name, ...) + async with |
| Resume a crashed run in place | await AgentSession.resume(actae, "exp-v1") |
| Fork an existing run at step N into a new experiment | await AgentSession.resume(actae, "exp-v1", fork_at_step=5, name="exp-v2") |
See docs/AGENT_SESSION.md for full lifecycle and fork semantics.
Framework Adapters
| Module | Class | Purpose |
|---|---|---|
actae_client.adapters.base |
StateManager |
Generic state save/load/resume |
actae_client.adapters.langgraph |
ActaeCheckpointSaver |
LangGraph 1.x checkpointer (full BaseCheckpointSaver protocol) |
actae_client.adapters.crewai |
ActaeCrewStateHook / CrewAIResumer |
CrewAI task state |
actae_client.adapters.langchain |
ActaeContextSaver / ChainResumer |
LangChain context |
actae_client.adapters.claude |
ActaeClaudeSessionStore / ActaeClaudeHook |
Claude Agent SDK durable transcripts + live hook events |
actae_client.adapters.openai_agents |
ActaeTracingProcessor / install_actae_tracing |
OpenAI Agents SDK trace/spans → events + run snapshots |
Adapters are lazy-imported: installing actae-client alone never requires
LangGraph, Claude, etc. Install what you need:
pip install 'actae-client[langgraph]' # LangGraph checkpointer
pip install 'actae-client[langchain]' # LangChain context callbacks
pip install 'actae-client[crewai]' # CrewAI state hooks
pip install 'actae-client[claude]' # Claude Agent SDK session store + hooks
pip install 'actae-client[openai-agents]' # OpenAI Agents SDK tracing
LangGraph checkpointer
from actae_client.adapters.langgraph import ActaeCheckpointSaver
saver = ActaeCheckpointSaver(actae_client)
graph = builder.compile(checkpointer=saver)
result = await graph.ainvoke(input, {"configurable": {"thread_id": "1"}})
# new process, same Actae — resume from the last checkpoint:
state = graph.get_state({"configurable": {"thread_id": "1"}})
result = graph.invoke(None, {"configurable": {"thread_id": "1"}})
Sync invoke() runs in a fresh event loop; calling sync protocol methods
inside a running loop raises ActaeLangGraphSyncError. See
docs/ADAPTERS.md for the storage format, channel resolution, and race
semantics.
Generic StateManager
from actae_client.adapters.base import StateManager
mgr = StateManager(actae_client, channel="my-agent")
state = await mgr.resume() # dict or {} when nothing saved yet
...
await mgr.save(state) # new version, returns version int
Dataclasses
Event
@dataclass
class Event:
id: str
channel_id: str
event_type: str
payload: Any
actor: str
cursor: int # gapless per-channel monotonic cursor
channel_cursor: Optional[int] # deprecated alias of cursor (same value)
timestamp: str
agent_id: Optional[str] = None
user_id: Optional[str] = None
metadata: Optional[Dict] = None
depends_on: Optional[str] = None # ID of the causally-depended-on parent event
HealthStatus
@dataclass
class HealthStatus:
status: str # "ok" | "degraded" | "unhealthy"
timestamp: str
instance_id: str
components: Dict[str, HealthComponent] # database, connections, system
check_duration_ms: int
ReadinessResult
@dataclass
class ReadinessResult:
status: str # "ready" | "not_ready"
timestamp: str
instance_id: str
check_duration_ms: int
database_ready: bool
capacity_available: bool
uptime_seconds: int
connection_utilization: float
active_connections: int
max_connections: int
MetricsSnapshot
@dataclass
class MetricsSnapshot:
status: str
timestamp: str
uptime_seconds: int
websocket_connections: int
topic_count: int
messages_sent: int
messages_received: int
total_messages: int
back_pressure: Dict[str, Any]
AuthResult / UserInfo / ChannelMetadata / BranchInfo / TransitionResult
@dataclass
class AuthResult:
user: UserInfo
token: str # JWT for subsequent auth calls
@dataclass
class UserInfo:
id: str
email: str
email_verified: bool
name: Optional[str]
image: Optional[str]
created_at: Optional[str]
@dataclass
class ChannelMetadata:
channel_id: str
parent_channel_id: Optional[str]
origin_run_id: str
forked_at_cursor: int
forked_at: str
display_name: Optional[str]
reason: Optional[str]
experiment_metadata: Optional[Dict[str, Any]]
@dataclass
class BranchInfo:
channel_id: str
display_name: Optional[str]
reason: Optional[str]
forked_at_cursor: int
event_count: int
latest_cursor: Optional[int]
children: List[BranchInfo] # recursive tree
@dataclass
class TransitionResult:
event: Event
state_version: int
Group, wake-up, and execution dataclasses: GroupInfo, GroupOffset,
ClaimedWork, Wakeup, ExecutionInfo, ExecutionClaim — see
SDK_REFERENCE.md for their fields.
Errors
All exceptions derive from ActaeError (which derives from Exception).
Notably, the WebSocket error is named ActaeConnectionError so it never
shadows Python's built-in ConnectionError.
| Exception | Raised when |
|---|---|
APIError(status_code, message) |
HTTP 4xx/5xx (all except the specific ones below) |
RateLimitError(retry_after_seconds, message) |
429 rate limited |
AuthError(message) |
HTTP 401 (bad/expired API key, wrong password on login) or WebSocket auth failure |
ActaeConnectionError(message) |
WebSocket failure / not connected, or HTTP transport failure (server down, timeout) |
SnapshotBoundaryError(message) |
Fork at_cursor has no saved state boundary |
VersionConflictError(message) |
Optimistic-concurrency guard mismatch |
ConsumerError(message) |
Consumer-group error (no lease, unknown group) |
IdempotencyKeyMismatchError(message) |
Execution key reused with different params |
ExecutionNotOwnedError(message) |
Stale claim token on an execution |
ExecutionNotFoundError(message) |
Execution id does not exist |
SessionError(message) |
AgentSession lifecycle violation |
SessionCompletedError(message) |
Step on a completed session |
ActaeLangGraphError(message) |
LangGraph checkpointer config/corruption |
ActaeLangGraphSyncError(message) |
Sync checkpointer call inside a running loop |
Dependencies
Only aiohttp is required. Framework adapters are optional and imported
lazily.
Execution-group members also provide subscribe_ws(), unsubscribe_ws(), and
stream_ws() for cursor-replay plus live WebSocket delivery of group messages.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file actae_client-1.2.0.tar.gz.
File metadata
- Download URL: actae_client-1.2.0.tar.gz
- Upload date:
- Size: 187.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
69c226a19702feb4446db742e19b814e7eb8f01bc46df314c79c0d933cf55e10
|
|
| MD5 |
2743708cf87d150e9d0a3e2f6ee59c19
|
|
| BLAKE2b-256 |
b9908812478264d47db1c78a711fc276b21a856e8e075f2e9b2fd66bf9f099b2
|
Provenance
The following attestation bundles were made for actae_client-1.2.0.tar.gz:
Publisher:
publish-sdks.yml on BViganotti/new_actae
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
actae_client-1.2.0.tar.gz -
Subject digest:
69c226a19702feb4446db742e19b814e7eb8f01bc46df314c79c0d933cf55e10 - Sigstore transparency entry: 2582838481
- Sigstore integration time:
-
Permalink:
BViganotti/new_actae@1e47fdc0a1b1f5f06be6812ff9c5e19bb571e349 -
Branch / Tag:
refs/tags/python-v1.2.0 - Owner: https://github.com/BViganotti
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-sdks.yml@1e47fdc0a1b1f5f06be6812ff9c5e19bb571e349 -
Trigger Event:
push
-
Statement type:
File details
Details for the file actae_client-1.2.0-py3-none-any.whl.
File metadata
- Download URL: actae_client-1.2.0-py3-none-any.whl
- Upload date:
- Size: 98.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4ace84babaefa669b27dbdaa625822a7bc42d8517e3640381002d727a569777f
|
|
| MD5 |
c3137aaf44815e6a1b6a09d5c9dca7b8
|
|
| BLAKE2b-256 |
3c807e9d5b96f15164bf90078685337eab249b9aa1065f841f08032f7d30d548
|
Provenance
The following attestation bundles were made for actae_client-1.2.0-py3-none-any.whl:
Publisher:
publish-sdks.yml on BViganotti/new_actae
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
actae_client-1.2.0-py3-none-any.whl -
Subject digest:
4ace84babaefa669b27dbdaa625822a7bc42d8517e3640381002d727a569777f - Sigstore transparency entry: 2582838496
- Sigstore integration time:
-
Permalink:
BViganotti/new_actae@1e47fdc0a1b1f5f06be6812ff9c5e19bb571e349 -
Branch / Tag:
refs/tags/python-v1.2.0 - Owner: https://github.com/BViganotti
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-sdks.yml@1e47fdc0a1b1f5f06be6812ff9c5e19bb571e349 -
Trigger Event:
push
-
Statement type: