Python SDK for controlling Happy agent sessions
Project description
happy-engineering-sdk
Python SDK for controlling Happy agent sessions.
Installation
pip install happy-engineering-sdk
Credentials
The SDK supports three ways to supply credentials.
1. Environment variables (recommended for containers)
export HAPPY_SERVER_URL=https://api.happy.engineering
export HAPPY_TOKEN=eyJ...
export HAPPY_SECRET=DroKzo0w...==
HAPPY_TOKEN is the bearer token and HAPPY_SECRET is the raw base64
machineKey string from your access.key file.
2. Key file (default for local use)
Download access.key (or agent.key) from the Happy dashboard and place it at:
~/.happy/access.key # written by the Happy CLI
~/.happy/agent.key # legacy location
The SDK understands both formats — the CLI-written access.key format
(encryption.machineKey) and the older agent.key format (secret).
Set the server URL:
export HAPPY_SERVER_URL=https://api.happy.engineering
3. Inline kwargs
client = HappyClient(
server_url="https://api.happy.engineering",
token="eyJ...",
secret_b64="DroKzo0w...==",
)
Quick start — async (HappyClient)
import asyncio
from happy_sdk import HappyClient
async def main():
client = HappyClient() # reads ~/.happy/agent.key + HAPPY_SERVER_URL
session_id = await client.run_task(
machine_id="my-machine",
directory="/home/user/project",
prompt="Summarise this week's PRs",
)
print(f"Task complete — session {session_id}")
asyncio.run(main())
Using environment variables:
client = HappyClient.from_env() # reads HAPPY_TOKEN, HAPPY_SECRET, HAPPY_SERVER_URL
Quick start — sync (SyncHappyClient)
For Django management commands, CLI scripts, or any sync context — use
SyncHappyClient. It has the same API as HappyClient but wraps every call
with asyncio.run() internally so you never touch async machinery:
from happy_sdk import SyncHappyClient
# From environment variables
client = SyncHappyClient.from_env()
session_id = client.run_task(
machine_id="my-machine",
directory="/home/user/project",
prompt="Summarise this week's PRs",
)
print(f"Task complete — session {session_id}")
All three constructor styles work with SyncHappyClient:
# From env vars
client = SyncHappyClient.from_env()
client = SyncHappyClient.from_env(server_url="https://...")
# From inline kwargs
client = SyncHappyClient(server_url="...", token="...", secret_b64="...")
# From key file
client = SyncHappyClient(server_url="...", credentials_path="~/.happy/access.key")
Manual session lifecycle
import asyncio
from happy_sdk import HappyClient
async def main():
client = HappyClient()
session_id = await client.spawn_session(
machine_id="my-machine",
directory="/home/user/project",
)
await client.send_message(session_id, "Hello")
await client.wait_for_turn_completion(session_id)
messages = await client.get_messages(session_id)
await client.stop_session(session_id)
asyncio.run(main())
API reference
HappyClient (async) / SyncHappyClient (sync)
Both classes expose identical method signatures. HappyClient methods are
async; SyncHappyClient methods are regular (blocking) functions.
Constructors
| Constructor | Description |
|---|---|
HappyClient(server_url=None, credentials_path=None, token=None, secret_b64=None) |
File or kwargs. token+secret_b64 take precedence over credentials_path. server_url falls back to HAPPY_SERVER_URL. |
HappyClient.from_env(server_url=None) |
Reads HAPPY_TOKEN, HAPPY_SECRET, HAPPY_SERVER_URL. Raises AuthenticationError if any are missing. |
SyncHappyClient(...) |
Same arguments as HappyClient. |
SyncHappyClient.from_env(server_url=None) |
Same as HappyClient.from_env. |
Session lifecycle
| Method | Signature | Description |
|---|---|---|
spawn_session |
(machine_id, directory, agent="claude", create_dir=False, name=None) → str |
Create a new agent session — returns the session ID. Pass name= to label it in the Happy apps (applied right after spawn) |
stop_session |
(session_id) |
Stop a running session |
delete_session |
(session_id) |
Permanently delete a session |
Naming & metadata
| Method | Signature | Description |
|---|---|---|
set_session_name |
(session_id, name) → Session |
Set the session's human-visible name (shown in the Happy web/mobile apps) |
update_session_metadata |
(session_id, changes: dict) → Session |
Merge changes into the session's metadata and persist it. Shallow merge (your keys win, others preserved), with optimistic-concurrency retries |
A session's name lives in its encrypted metadata rather than being a spawn-time
argument, so naming is a quick follow-up write after the session exists. The
Happy apps show metadata.summary.text as the session title, so
set_session_name writes the name there (and mirrors it to metadata.name for
read-back):
sid = await client.spawn_session(machine_id, "/repo", name="Nightly build")
# ...or rename later:
await client.set_session_name(sid, "Nightly build (retry)")
# read it back:
session = await client.get_session(sid)
print(session.metadata["summary"]["text"], session.metadata_version)
Messaging
| Method | Signature | Description |
|---|---|---|
send_message |
(session_id, text, permission_mode="yolo", *, confirm=True, confirm_timeout=10.0, poll_interval=1.5, max_attempts=3) |
Send a message and confirm it was delivered. Returns once the server has persisted the message, or raises MessageDeliveryError if it can't be confirmed after retrying. Pass confirm=False for best-effort fire-and-forget |
Confirmed delivery. By default send_message doesn't just fire the message —
it verifies the server actually received and stored it, retrying transparently
if not. This matters most right after spawn_session: a naive send there is
easily lost, but the confirmed send (also used by run_task for its initial
prompt) reliably lands. "Delivered" means persisted by the server — the agent
reads persisted messages on its own; it does not mean the agent has replied yet.
Tune the bounds with the keyword-only params, or set confirm=False to opt out
(that path still flushes the socket so it won't silently drop).
Waiting
| Method | Signature | Description |
|---|---|---|
wait_for_turn_completion |
(session_id, timeout_seconds=300) |
Block until the agent finishes its current turn |
wait_for_idle |
(session_id, timeout_seconds=300) |
Block until the session enters an idle state |
Query
| Method | Signature | Description |
|---|---|---|
list_sessions |
(active_only=False) → list[Session] |
List all (or only active) sessions |
get_session |
(session_id) → Session |
Fetch a single session — raises SessionNotFound if it doesn't exist |
is_alive |
(session_id) → bool |
Whether the session is currently active on the server |
get_messages |
(session_id) → list[Message] |
Fetch all messages for a session |
list_machines |
(active_only=False) → list[Machine] |
List all (or only active) machines |
get_machine |
(machine_id) → Machine |
Fetch a single machine |
Convenience
| Method | Signature | Description |
|---|---|---|
run_task |
(machine_id, directory, prompt, agent="claude", timeout_seconds=600) → str |
Spawn, send, wait, stop — returns session ID |
Cleanup
| Method | Signature | Description |
|---|---|---|
close |
() |
Release any held resources (no-op in the current implementation) |
Types
| Type | Fields |
|---|---|
Session |
id: str, active: bool, created_at: int, metadata: dict (decrypted; the name is metadata["name"]), agent_state: str | None, metadata_version: int |
Machine |
id: str, active: bool, metadata: dict |
Message |
id: str, seq: int, content: dict, created_at: int |
Agent |
Literal["claude"] |
PermissionMode |
Literal["yolo"] |
Exceptions
All exceptions inherit from HappyError.
| Exception | Raised when |
|---|---|
AuthenticationError |
Credentials missing, expired, or malformed |
MachineOfflineError |
Target machine is not connected to the server |
SessionNotFound |
No session with the given id exists on the server |
SpawnError |
Session spawn failed |
TimeoutError |
Wait exceeded the specified timeout |
EncryptionError |
Encrypt or decrypt operation failed |
ConnectionError |
Socket connection failed or disconnected unexpectedly |
MetadataUpdateError |
The server rejected a session metadata update (or returned a malformed ack) |
MetadataConflictError |
A metadata update lost too many optimistic-concurrency races to complete |
MessageDeliveryError |
A message could not be confirmed delivered after the retry budget (see send_message) |
License
MIT — see LICENSE.
Project details
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 happy_engineering_sdk-0.4.0.tar.gz.
File metadata
- Download URL: happy_engineering_sdk-0.4.0.tar.gz
- Upload date:
- Size: 63.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b9092224c19005470b16c0e173832a99d653a712edc35454617d0fc3cd32a1d8
|
|
| MD5 |
a8ae01149219679601de17531c2f26e8
|
|
| BLAKE2b-256 |
913aa94e2b4b6ee15f5d458cc2a1f09cf3a0fc16dcc566afd98ee0fa349cdbd3
|
File details
Details for the file happy_engineering_sdk-0.4.0-py3-none-any.whl.
File metadata
- Download URL: happy_engineering_sdk-0.4.0-py3-none-any.whl
- Upload date:
- Size: 19.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e6d46fe9e6fd796ca0c79bce3e3d48f8c51e2c7f3e862c8c40741f86dc571930
|
|
| MD5 |
3684a8fdc0fe31c2366c37fa7c0e14cf
|
|
| BLAKE2b-256 |
11a676c7ca66ba197e3be77909a653b6b96d8939be90207e135206872a429917
|