> ## Documentation Index
> Fetch the complete documentation index at: https://docs.guidinghand.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# JavaScript and TypeScript

> The guidinghand package for Node, Deno, Bun and edge runtimes.

```bash theme={null}
npm install guidinghand
```

No dependencies, types included. It runs on Node 18+, Deno, Bun and Cloudflare Workers. Keep your API key on the server: the SDK isn't meant for browsers.

## Quickstart

```ts theme={null}
import GuidingHand from 'guidinghand';

const gh = new GuidingHand(); // reads GUIDINGHAND_API_KEY

// 1. A session: send its invite link to the person at the computer
const session = await gh.sessions.create({ agent_id: 'default', metadata: { ticket: 'T-123' } });
console.log('Send this link:', session.invite_url);

// 2. Wait for their computer to connect
await gh.sessions.waitForConnection(session.session_id);

// 3. Run a task, answering the agent's questions and approvals as they come
const task = await gh.tasks.run(session.session_id, {
  prompt: 'Turn on Dark Mode',
  onEvent: (e) => console.log(e.type, e.message),
  onQuestion: async (q) => 'Work',
  onApproval: async (a) => a.risk !== 'high',
});

console.log(task.status, task.result, task.replay_url);
```

`run()` starts the task and follows it to the end. When the agent asks something, it calls `onQuestion` and sends back what you return. When the agent wants to do something consequential, it calls `onApproval`: return `true` or `'approve'` to allow it, `false` or `'deny'` to refuse, or `{ decision: 'deny', note: 'why' }`. If a question or approval comes up and you didn't pass a handler, `run()` throws `NeedsInputError` (with `error.task`) instead of waiting forever, except for questions and approvals the customer can answer or decide on their screen (below).

## Questions and approvals the customer answers

Unless the agent turns it off ([`customer_answers: false`](/concepts/agents#questions-the-customer-can-answer)), the agent's questions also appear on the customer's screen, and they can answer them there. `question.customer_can_answer` says whether they can answer the open one. The first answer is used, from them or from you.

* Without `onQuestion`, `run()` leaves those questions to the customer and keeps following the task (pass `timeout` to give up eventually). It throws `NeedsInputError` only for a question they can't answer.
* With `onQuestion`, return `null` to leave one to them. If they answer while your handler works, your answer is refused with a `409` and `run()` carries on.
* `gh.tasks.respond()` after they answered throws `ConflictError` with `extra.code === 'already_answered'` and `extra.answered_by === 'customer'`.
* The `answer` event has `data.answered_by` (`'customer'` or `'operator'`), and the `task.question_answered` webhook carries the answer and who gave it.

Approvals work the same way. Unless the agent turns it off ([`customer_approvals: false`](/concepts/agents#approvals-the-customer-can-decide)), the customer can approve or deny the agent's approval requests on their screen, and `approval.customer_can_approve` says whether they can decide the open one.

* Without `onApproval`, `run()` leaves those approvals to the customer and keeps following. With `onApproval`, return `null` to leave one to them.
* `gh.tasks.respond()` after they decided throws `ConflictError` with `extra.code === 'already_answered'` and `extra.answered_by === 'customer'`.
* The `approved` and `denied` events have `data.answered_by`, and the `task.approval_decided` webhook carries the decision and who made it.

```ts theme={null}
const task = await gh.tasks.run(session.session_id, {
  prompt: 'Set up the office printer',
  onEvent: (e) => { if (e.type === 'answer') console.log(`${e.data?.answered_by} answered: ${e.message}`); },
  onQuestion: async (q) => (q.customer_can_answer ? null : askMyTeam(q.question, q.options)),
  onApproval: async (a) => (a.customer_can_approve && a.risk !== 'high' ? null : askMyTeamToApprove(a.action)),
});
```

## Client

```ts theme={null}
new GuidingHand({
  apiKey: process.env.GUIDINGHAND_API_KEY, // default
  baseUrl: 'https://guidinghand.ai',        // or https://dev.guidinghand.ai
  timeout: 60_000,                          // per request, in ms
  maxRetries: 2,                            // connection errors, 429 and 5xx
});
```

Reads, deletes and task starts that carry a `request_id` are retried. Other writes are not.

## Resources

| Method                                                                                                                                        | API                                                     |
| --------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- |
| `gh.agents.list()`                                                                                                                            | `GET /v1/agents`                                        |
| `gh.agents.create({ name, agent_id?, instructions?, effort?, greeting?, display_name?, narration?, customer_answers?, customer_approvals? })` | `POST /v1/agents`                                       |
| `gh.agents.retrieve(agentId)` / `.update(agentId, fields)` / `.delete(agentId)`                                                               | `GET`, `PATCH`, `DELETE /v1/agents/{agent_id}`          |
| `gh.sessions.create({ agent_id?, metadata? })`                                                                                                | `POST /v1/sessions`                                     |
| `gh.sessions.list({ agent_id?, limit?, cursor? })`, `.listAll({ agent_id? })`                                                                 | `GET /v1/sessions`                                      |
| `gh.sessions.retrieve(id)` / `.delete(id)`                                                                                                    | `GET`, `DELETE /v1/sessions/{session_id}`               |
| `gh.sessions.waitForConnection(id, { timeout?, pollInterval? })`                                                                              | polls until `status` is `connected`                     |
| `gh.tasks.create(sessionId, { prompt, agent_id?, request_id?, metadata? })`                                                                   | `POST /v1/sessions/{session_id}/tasks`                  |
| `gh.tasks.run(sessionId, { prompt, onEvent?, onQuestion?, onApproval?, timeout? })`                                                           | create, then follow and answer to the end               |
| `gh.tasks.list({ session_id?, agent_id?, status?, limit?, cursor? })`, `.listAll(...)`                                                        | `GET /v1/tasks`                                         |
| `gh.tasks.retrieve(taskId, { include: ['events', 'trace'] })`                                                                                 | `GET /v1/tasks/{task_id}`                               |
| `gh.tasks.events(taskId, { after, wait_ms })`                                                                                                 | `GET /v1/tasks/{task_id}/events`                        |
| `gh.tasks.stream(taskId)`                                                                                                                     | an async iterator of events until the task is done      |
| `gh.tasks.respond(taskId, { question_id, answer } \| { approval_id, decision, note? })`                                                       | `POST /v1/tasks/{task_id}/respond`                      |
| `gh.tasks.stop(taskId)`                                                                                                                       | `POST /v1/tasks/{task_id}/stop`                         |
| `gh.tasks.recording(taskId)` / `.recordingFrame(taskId, seq)`                                                                                 | `GET /v1/tasks/{task_id}/recording[/{seq}]` (PNG bytes) |
| `gh.webhook.retrieve()` / `.update({ url, events?, rotate_secret? })` / `.delete()`                                                           | `GET`, `PUT /v1/webhook`                                |

`listAll()` walks every page for you:

```ts theme={null}
for await (const t of gh.tasks.listAll({ agent_id: 'billing', status: 'completed' })) {
  console.log(t.task_id, t.result);
}
```

## Webhooks

```ts theme={null}
import { verifyWebhook } from 'guidinghand';

app.post('/guidinghand', express.raw({ type: 'application/json' }), async (req, res) => {
  const event = await verifyWebhook(req.body, req.header('GuidingHand-Signature'), process.env.GUIDINGHAND_WEBHOOK_SECRET!);
  if (event.type === 'task.completed') console.log(event.data.task!.result);
  if (event.type === 'task.question_answered') console.log(event.data.answer!.answered_by, event.data.answer!.answer);
  if (event.type === 'task.approval_decided') console.log(event.data.decision!.answered_by, event.data.decision!.decision);
  res.sendStatus(200);
});
```

`verifyWebhook` needs the raw body, compares in constant time and rejects deliveries older than 5 minutes (`{ tolerance }` in seconds). It throws `WebhookVerificationError`. See [Webhooks](/guides/webhooks).

## Errors

Every error extends `GuidingHandError`, with `status`, `type`, `message` and `extra`: `InvalidRequestError` (400), `AuthenticationError` (401), `PaymentRequiredError` (402), `PermissionDeniedError` (403), `NotFoundError` (404), `ConflictError` (409, for example `extra.active_task_id` when a task is already running, or `extra.code: 'already_answered'` with `extra.answered_by` when someone answered a question or decided an approval first), `RateLimitError` (429), `APIError` (5xx), `APIConnectionError`, `TimeoutError`, `NeedsInputError` and `SessionExpiredError`.

```ts theme={null}
import { ConflictError } from 'guidinghand';

try {
  await gh.tasks.create(sessionId, { prompt: 'Update the printer driver' });
} catch (e) {
  if (e instanceof ConflictError && e.extra.active_task_id) await gh.tasks.stop(String(e.extra.active_task_id));
  else throw e;
}
```
