Quickstart
Create an org API key in the console under Settings → API keys. It starts withgh_live_ and is shown once.
TypedDicts (Agent, Session, Task, Event, Recording, Webhook, Page, …), so editors and type checkers know the keys. Field names are the API’s own (session_id, task_id, agent_id, …), and so are the parameter names.
The client
AuthenticationError. The client holds a connection pool: use it as a context manager or call client.close() when you are done.
AsyncGuidingHand has the same resources and methods, and you await them. See Async.
Agents
An agent holds your instructions, reasoning effort, the greeting shown on the invite page, and what the person at the computer sees and may do. Every org has adefault agent that works without setup.
agent["invite_url_template"] is the agent’s invite link with a {code} placeholder. Creating an agent_id that already exists raises ConflictError.
Sessions
A session is one pairing code (session_id, e.g. K7QM-24XP) for one computer. Codes expire after 72 hours without use.
wait_for_connection returns the session once status is connected and device is set. It raises SessionExpired if the code expires first, and GuidingHandTimeout after timeout seconds (timeout=None waits indefinitely). As an alternative to polling, a webhook sends session.connected.
Tasks
A task is a prompt carried out on the session’s computer. Each computer runs one task at a time.Run a task to the end
tasks.run starts a task and follows it until it is done, then returns the finished task.
on_question returns the answer as a string (or None to leave it to the customer, below). on_approval returns True or False, "approve" or "deny", or {"decision": "deny", "note": "Not on a Friday"} to pass a note to the agent (or None to leave it to the customer). With GuidingHand the handlers are plain functions: an async def handler raises TypeError, so use AsyncGuidingHand for those. If the agent needs a handler you didn’t pass, run raises NeedsInput instead of hanging. The task keeps waiting on the server, so you can answer it yourself:
Questions and approvals the customer answers
Unless the agent turns it off (customer_answers=False), the agent’s questions also appear on the customer’s screen, and they can answer them there. pending["customer_can_answer"] says whether they can answer the open one. The first answer is used, from them or from you.
- Without
on_question,runleaves those questions to the customer and keeps following the task (passtimeoutto give up eventually). It raisesNeedsInputonly for a question they can’t answer. - With
on_question, returnNoneto leave one to them. If they answer while your handler works, your answer is refused with a 409 andruncarries on. tasks.respondafter they answered raisesConflictErrorwithe.extra["code"] == "already_answered"ande.extra["answered_by"] == "customer".- The
answerevent hasdata["answered_by"]("customer"or"operator"), and thetask.question_answeredwebhook carries the answer and who gave it.
customer_approvals=False), the customer can approve or deny the agent’s approval requests on their screen, and pending["customer_can_approve"] says whether they can decide the open one.
- Without
on_approval,runleaves those approvals to the customer and keeps following. Withon_approval, returnNoneto leave one to them. tasks.respondafter they decided raisesConflictErrorwithe.extra["code"] == "already_answered"ande.extra["answered_by"] == "customer".- The
approvedanddeniedevents havedata["answered_by"], and thetask.approval_decidedwebhook carries the decision and who made it.
Follow a task yourself
tasks.events long-polls GET /v1/tasks/{id}/events. Each page has the new events and the task as it is now, so you can answer what it is waiting on:
task["pending"], not the question and approval_required events: the events start from the beginning, so a re-run (the same request_id returns the same task) sees questions that were already answered.
To only watch, stream(task_id, after=0) yields each event once and ends when the task is done. To resume from a known point, pass the last cursor you have as after.
Everything else
task["replay_url"] opens the replay in the console: every screen the agent saw, with its cursor and clicks.
Pagination
Lists return one page, newest first:sessions.list_all(...) and tasks.list_all(...) take the same filters and fetch pages as you iterate. They also take limit= for the page size.
Webhooks
Instead of polling, GuidingHand can POST events to your endpoint:session.connected, session.disconnected, task.started, task.waiting_for_user, task.question_answered, task.waiting_for_approval, task.approval_decided, task.completed, task.failed and task.stopped.
update changes only what you pass: leave out url or events to keep them (the first call needs a url). events limits which events are sent, and [] means all of them (a new endpoint starts with all). An unknown event type raises InvalidRequestError. The URL must be a public https:// address.
Each delivery carries a GuidingHand-Signature: t=<unix>,v1=<hex> header. Verify it against the raw request body before trusting the event:
verify_webhook(payload, signature_header, secret, tolerance=300) compares signatures in constant time. It rejects timestamps more than tolerance seconds from now (tolerance=None turns that check off) and returns the parsed event: {"id", "type", "created_at", "org_id", "data": {"task": ...} | {"session": ...}} (plus data["answer"] for task.question_answered and data["decision"] for task.approval_decided). Deliveries that don’t get a 2xx are retried up to 4 times over about 3 minutes, so use event["id"] to ignore repeats.
Errors
Every exception is aGuidingHandError, with message, status (the HTTP status, or None), type (the API’s error type), body (the parsed response) and extra (the error’s other fields). Exceptions can be pickled, e.g. to pass them between processes.
Retries
Requests that are safe to repeat are retried up tomax_retries times (default 2) on connection errors, 429 and 5xx. The waits grow 0.5 s, 1 s, 2 s, and so on, and a Retry-After header takes precedence. Safe requests are GET and DELETE, plus POSTs that carry a request_id (starting a task with request_id is idempotent). Other POSTs, PATCH and PUT are never retried. To make starting a task safe to retry, pass a request_id. If a DELETE’s answer is lost and the retry finds the session or agent already gone, the delete returns its usual {"deleted": True} result.
Async
AsyncGuidingHand mirrors GuidingHand. stream and list_all are async iterators, and tasks.run accepts both plain functions and coroutine functions as handlers.
Development
The tests start the real server (node server/src/index.js with an in-memory database), a fake OpenAI, a webhook receiver and fake computers on the WebSocket bridge. They need Node 18+ and server/node_modules.
NODE=/path/to/node to choose the Node binary.