Skip to main content

Python SDK for controlling Happy agent sessions

Project description

happy-engineering-sdk

PyPI version Python versions License: MIT

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

happy_engineering_sdk-0.4.0.tar.gz (63.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

happy_engineering_sdk-0.4.0-py3-none-any.whl (19.4 kB view details)

Uploaded Python 3

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

Hashes for happy_engineering_sdk-0.4.0.tar.gz
Algorithm Hash digest
SHA256 b9092224c19005470b16c0e173832a99d653a712edc35454617d0fc3cd32a1d8
MD5 a8ae01149219679601de17531c2f26e8
BLAKE2b-256 913aa94e2b4b6ee15f5d458cc2a1f09cf3a0fc16dcc566afd98ee0fa349cdbd3

See more details on using hashes here.

File details

Details for the file happy_engineering_sdk-0.4.0-py3-none-any.whl.

File metadata

File hashes

Hashes for happy_engineering_sdk-0.4.0-py3-none-any.whl
Algorithm Hash digest
SHA256 e6d46fe9e6fd796ca0c79bce3e3d48f8c51e2c7f3e862c8c40741f86dc571930
MD5 3684a8fdc0fe31c2366c37fa7c0e14cf
BLAKE2b-256 11a676c7ca66ba197e3be77909a653b6b96d8939be90207e135206872a429917

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page