guidinghand 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -6
- package/dist/cjs/index.d.ts +59 -14
- package/dist/cjs/index.js +25 -11
- package/dist/esm/index.d.ts +59 -14
- package/dist/esm/index.js +25 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -26,8 +26,8 @@ await gh.sessions.waitForConnection(session.session_id);
|
|
|
26
26
|
const task = await gh.tasks.run(session.session_id, {
|
|
27
27
|
prompt: 'Turn on Dark Mode',
|
|
28
28
|
onEvent: (e) => console.log(e.type, e.message),
|
|
29
|
-
onQuestion: async (q) => 'Work', // q.question, q.options
|
|
30
|
-
onApproval: async (a) => a.risk !== 'high', // true/'approve' or false/'deny'
|
|
29
|
+
onQuestion: async (q) => 'Work', // q.question, q.options (leave it out to let the customer answer on their screen)
|
|
30
|
+
onApproval: async (a) => a.risk !== 'high', // true/'approve' or false/'deny' (leave it out to let the customer decide on their screen)
|
|
31
31
|
});
|
|
32
32
|
|
|
33
33
|
console.log(task.status, task.result); // completed "Dark Mode is on."
|
|
@@ -36,11 +36,12 @@ console.log('Replay:', task.replay_url);
|
|
|
36
36
|
|
|
37
37
|
## Agents
|
|
38
38
|
|
|
39
|
-
An agent is your configuration: instructions (added under GuidingHand's own rules, which always win), effort
|
|
39
|
+
An agent is your configuration: instructions (added under GuidingHand's own rules, which always win), effort, a greeting for the invite page, and what the customer sees and may do on their screen. Every org has a `default` agent.
|
|
40
40
|
|
|
41
41
|
```ts
|
|
42
42
|
await gh.agents.create({ agent_id: 'billing', name: 'Billing help', instructions: 'Open the Billing app from the Dock.', effort: 'medium', greeting: 'Hi, this is Acme billing.' });
|
|
43
43
|
await gh.agents.update('billing', { effort: 'high' });
|
|
44
|
+
await gh.agents.update('billing', { display_name: 'Acme Support', narration: true, customer_answers: false, customer_approvals: false }); // on their screen
|
|
44
45
|
const { data } = await gh.agents.list();
|
|
45
46
|
await gh.agents.delete('billing');
|
|
46
47
|
```
|
|
@@ -72,6 +73,30 @@ const page = await gh.tasks.list({ status: 'completed', limit: 50 }); // { da
|
|
|
72
73
|
for await (const t of gh.tasks.listAll({ agent_id: 'billing' })) { /* every page */ }
|
|
73
74
|
```
|
|
74
75
|
|
|
76
|
+
## Questions and approvals the customer answers
|
|
77
|
+
|
|
78
|
+
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. `question.customer_can_answer` says whether they can answer the open one. The first answer is used, from them or from you.
|
|
79
|
+
|
|
80
|
+
- 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.
|
|
81
|
+
- With `onQuestion`, return `null` to leave one to them (say, when `q.customer_can_answer` is true). If they answer while your handler works, your answer is refused with a 409 and `run()` carries on.
|
|
82
|
+
- `respond()` after they answered throws `ConflictError` with `extra.code === 'already_answered'` and `extra.answered_by === 'customer'`.
|
|
83
|
+
- The `answer` event has `data.answered_by` (`'customer'` or `'operator'`), and the `task.question_answered` webhook carries the answer and who gave it.
|
|
84
|
+
|
|
85
|
+
Approvals work the same way. Unless the agent turns it off (`customer_approvals: false`), 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.
|
|
86
|
+
|
|
87
|
+
- Without `onApproval`, `run()` leaves those approvals to the customer and keeps following. With `onApproval`, return `null` to leave one to them.
|
|
88
|
+
- `respond()` after they decided throws `ConflictError` with `extra.code === 'already_answered'` and `extra.answered_by === 'customer'`.
|
|
89
|
+
- The `approved` and `denied` events have `data.answered_by`, and the `task.approval_decided` webhook carries the decision and who made it.
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
const task = await gh.tasks.run(sessionId, {
|
|
93
|
+
prompt: 'Set up the office printer',
|
|
94
|
+
onEvent: (e) => { if (e.type === 'answer') console.log(`${e.data?.answered_by} answered: ${e.message}`); },
|
|
95
|
+
onQuestion: async (q) => (q.customer_can_answer ? null : askMyTeam(q.question, q.options)),
|
|
96
|
+
onApproval: async (a) => (a.customer_can_approve && a.risk !== 'high' ? null : askMyTeamToApprove(a.action)),
|
|
97
|
+
});
|
|
98
|
+
```
|
|
99
|
+
|
|
75
100
|
## Recordings
|
|
76
101
|
|
|
77
102
|
```ts
|
|
@@ -84,7 +109,7 @@ The console replays them with the agent's cursor at `task.replay_url`.
|
|
|
84
109
|
## Webhooks
|
|
85
110
|
|
|
86
111
|
```ts
|
|
87
|
-
const { secret } = await gh.webhook.update({ url: 'https://example.com/guidinghand', events: ['task.completed', 'task.waiting_for_user'] });
|
|
112
|
+
const { secret } = await gh.webhook.update({ url: 'https://example.com/guidinghand', events: ['task.completed', 'task.waiting_for_user', 'task.question_answered', 'task.approval_decided'] });
|
|
88
113
|
```
|
|
89
114
|
|
|
90
115
|
Verify each delivery with the raw request body:
|
|
@@ -95,6 +120,8 @@ import { verifyWebhook } from 'guidinghand';
|
|
|
95
120
|
app.post('/guidinghand', express.raw({ type: 'application/json' }), async (req, res) => {
|
|
96
121
|
const event = await verifyWebhook(req.body, req.header('GuidingHand-Signature'), process.env.GUIDINGHAND_WEBHOOK_SECRET);
|
|
97
122
|
if (event.type === 'task.completed') console.log(event.data.task.result);
|
|
123
|
+
if (event.type === 'task.question_answered') console.log(event.data.answer.answered_by, event.data.answer.answer);
|
|
124
|
+
if (event.type === 'task.approval_decided') console.log(event.data.decision.answered_by, event.data.decision.decision);
|
|
98
125
|
res.sendStatus(200);
|
|
99
126
|
});
|
|
100
127
|
```
|
|
@@ -110,11 +137,11 @@ Every error is a `GuidingHandError` with `status`, `type`, `message` and `extra`
|
|
|
110
137
|
| `PaymentRequiredError` | 402 | the org's plan doesn't allow it (free minutes used up) |
|
|
111
138
|
| `PermissionDeniedError` | 403 | your role can't do this |
|
|
112
139
|
| `NotFoundError` | 404 | not in this org |
|
|
113
|
-
| `ConflictError` | 409 | no computer connected, a task already running (`extra.active_task_id`), nothing pending |
|
|
140
|
+
| `ConflictError` | 409 | no computer connected, a task already running (`extra.active_task_id`), nothing pending, or already answered or decided (`extra.code: 'already_answered'`, `extra.answered_by`) |
|
|
114
141
|
| `RateLimitError` | 429 | too many requests, or the plan's tasks at once |
|
|
115
142
|
| `APIError` | 5xx | our side |
|
|
116
143
|
| `APIConnectionError`, `TimeoutError` | | network |
|
|
117
|
-
| `NeedsInputError` | | `run()` got a question or approval without a handler (`error.task`) |
|
|
144
|
+
| `NeedsInputError` | | `run()` got a question or approval (that the customer can't answer or decide) without a handler (`error.task`) |
|
|
118
145
|
| `SessionExpiredError` | | `waitForConnection()` on a code that expired |
|
|
119
146
|
|
|
120
147
|
Reads, deletes and starts with a `request_id` are retried on connection errors, 429 and 5xx (`maxRetries`, default 2).
|
package/dist/cjs/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const VERSION = "0.
|
|
1
|
+
export declare const VERSION = "0.3.0";
|
|
2
2
|
export type Metadata = Record<string, string>;
|
|
3
3
|
export type Effort = 'low' | 'medium' | 'high' | null;
|
|
4
4
|
export type Agent = {
|
|
@@ -8,6 +8,14 @@ export type Agent = {
|
|
|
8
8
|
instructions: string;
|
|
9
9
|
effort: Effort;
|
|
10
10
|
greeting: string;
|
|
11
|
+
/** What the app calls the agent on the person's screen; '' is "GuidingHand". */
|
|
12
|
+
display_name: string;
|
|
13
|
+
/** The app shows the agent's thoughts and steps on the person's screen as it works. */
|
|
14
|
+
narration: boolean;
|
|
15
|
+
/** The person at the computer can answer the agent's questions in the app (the team still can; the first answer is used). */
|
|
16
|
+
customer_answers: boolean;
|
|
17
|
+
/** The person at the computer can approve or deny the agent's approval requests in the app (the team still can; the first decision is used). */
|
|
18
|
+
customer_approvals: boolean;
|
|
11
19
|
is_default: boolean;
|
|
12
20
|
invite_url_template: string;
|
|
13
21
|
created_at: string | null;
|
|
@@ -38,19 +46,29 @@ export type Session = {
|
|
|
38
46
|
latest_task_id?: string | null;
|
|
39
47
|
};
|
|
40
48
|
export type TaskStatus = 'queued' | 'running' | 'waiting_for_user' | 'waiting_for_approval' | 'completed' | 'failed' | 'stopped';
|
|
49
|
+
/** customer_can_answer: the person at the computer can answer it on their screen too (the first answer is used). */
|
|
41
50
|
export type Question = {
|
|
42
51
|
type: 'question';
|
|
43
52
|
question_id: string;
|
|
44
53
|
question: string;
|
|
45
54
|
options: string[];
|
|
55
|
+
customer_can_answer: boolean;
|
|
46
56
|
};
|
|
57
|
+
/** customer_can_approve: the person at the computer can approve or deny it on their screen too (the first decision is used). */
|
|
47
58
|
export type Approval = {
|
|
48
59
|
type: 'approval';
|
|
49
60
|
approval_id: string;
|
|
50
61
|
action: string;
|
|
51
62
|
risk: 'low' | 'medium' | 'high';
|
|
63
|
+
customer_can_approve: boolean;
|
|
52
64
|
};
|
|
53
65
|
export type EventType = 'started' | 'progress' | 'thinking' | 'action' | 'message' | 'question' | 'answer' | 'approval_required' | 'approved' | 'denied' | 'completed' | 'error' | 'stopped';
|
|
66
|
+
/** Who answered a question or decided an approval: the person at the computer ('customer', in the app) or your team ('operator': the API or the console). */
|
|
67
|
+
export type AnsweredBy = 'customer' | 'operator';
|
|
68
|
+
/**
|
|
69
|
+
* data: e.g. { action } for 'action', { question_id, question, options } for 'question', { question_id, answered_by } for 'answer'
|
|
70
|
+
* (whose message is the answer), { approval_id, answered_by, note? } for 'approved' and 'denied'.
|
|
71
|
+
*/
|
|
54
72
|
export type TaskEvent = {
|
|
55
73
|
cursor: number;
|
|
56
74
|
type: EventType;
|
|
@@ -96,7 +114,7 @@ export type Recording = {
|
|
|
96
114
|
task_id: string;
|
|
97
115
|
frames: Frame[];
|
|
98
116
|
};
|
|
99
|
-
export type WebhookEventType = 'session.connected' | 'session.disconnected' | 'task.started' | 'task.waiting_for_user' | 'task.waiting_for_approval' | 'task.completed' | 'task.failed' | 'task.stopped';
|
|
117
|
+
export type WebhookEventType = 'session.connected' | 'session.disconnected' | 'task.started' | 'task.waiting_for_user' | 'task.question_answered' | 'task.waiting_for_approval' | 'task.approval_decided' | 'task.completed' | 'task.failed' | 'task.stopped';
|
|
100
118
|
export type Webhook = {
|
|
101
119
|
object: 'webhook';
|
|
102
120
|
url: string | null;
|
|
@@ -105,6 +123,18 @@ export type Webhook = {
|
|
|
105
123
|
secret?: string;
|
|
106
124
|
event_types: WebhookEventType[];
|
|
107
125
|
};
|
|
126
|
+
export type QuestionAnswer = {
|
|
127
|
+
question_id: string;
|
|
128
|
+
answer: string;
|
|
129
|
+
answered_by: AnsweredBy;
|
|
130
|
+
};
|
|
131
|
+
export type ApprovalDecision = {
|
|
132
|
+
approval_id: string;
|
|
133
|
+
decision: 'approve' | 'deny';
|
|
134
|
+
note: string | null;
|
|
135
|
+
answered_by: AnsweredBy;
|
|
136
|
+
};
|
|
137
|
+
/** data.answer: for task.question_answered. data.decision: for task.approval_decided. */
|
|
108
138
|
export type WebhookEvent = {
|
|
109
139
|
id: string;
|
|
110
140
|
type: WebhookEventType;
|
|
@@ -113,6 +143,8 @@ export type WebhookEvent = {
|
|
|
113
143
|
data: {
|
|
114
144
|
task?: Task;
|
|
115
145
|
session?: Session;
|
|
146
|
+
answer?: QuestionAnswer;
|
|
147
|
+
decision?: ApprovalDecision;
|
|
116
148
|
};
|
|
117
149
|
};
|
|
118
150
|
export type Page<T> = {
|
|
@@ -141,6 +173,7 @@ export declare class PermissionDeniedError extends GuidingHandError {
|
|
|
141
173
|
}
|
|
142
174
|
export declare class NotFoundError extends GuidingHandError {
|
|
143
175
|
}
|
|
176
|
+
/** 409. From respond(): extra.code is 'already_answered' when someone answered or decided first (extra.answered_by: 'customer' is the person at the computer), else 'not_pending'. */
|
|
144
177
|
export declare class ConflictError extends GuidingHandError {
|
|
145
178
|
}
|
|
146
179
|
export declare class RateLimitError extends GuidingHandError {
|
|
@@ -153,7 +186,7 @@ export declare class TimeoutError extends GuidingHandError {
|
|
|
153
186
|
}
|
|
154
187
|
export declare class SessionExpiredError extends GuidingHandError {
|
|
155
188
|
}
|
|
156
|
-
/** A task waits on a question or approval and `run()` got no handler for it. */
|
|
189
|
+
/** A task waits on a question or approval and `run()` got no handler for it (one the person at the computer can answer or decide is left to them instead). */
|
|
157
190
|
export declare class NeedsInputError extends GuidingHandError {
|
|
158
191
|
task: Task;
|
|
159
192
|
constructor(task: Task);
|
|
@@ -205,6 +238,10 @@ export type AgentCreate = {
|
|
|
205
238
|
instructions?: string;
|
|
206
239
|
effort?: Effort;
|
|
207
240
|
greeting?: string;
|
|
241
|
+
display_name?: string;
|
|
242
|
+
narration?: boolean;
|
|
243
|
+
customer_answers?: boolean;
|
|
244
|
+
customer_approvals?: boolean;
|
|
208
245
|
};
|
|
209
246
|
export type AgentUpdate = Partial<Omit<AgentCreate, 'agent_id'>>;
|
|
210
247
|
declare class Agents extends Resource {
|
|
@@ -261,19 +298,27 @@ export type Respond = {
|
|
|
261
298
|
decision: 'approve' | 'deny';
|
|
262
299
|
note?: string;
|
|
263
300
|
};
|
|
301
|
+
/** null leaves the approval to the person at the computer when they can decide it (customer_can_approve); otherwise it denies. */
|
|
302
|
+
export type ApprovalReply = boolean | 'approve' | 'deny' | {
|
|
303
|
+
decision: 'approve' | 'deny';
|
|
304
|
+
note?: string;
|
|
305
|
+
} | null | undefined;
|
|
264
306
|
export type RunOptions = TaskCreate & {
|
|
265
307
|
/** Every event, as it happens. */
|
|
266
308
|
onEvent?: (event: TaskEvent, task: Task) => void | Promise<void>;
|
|
267
|
-
/**
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
309
|
+
/**
|
|
310
|
+
* The agent asks something: return the answer. When the person at the computer can answer it on their screen
|
|
311
|
+
* (question.customer_can_answer), return null to leave it to them; without onQuestion, run() does that for
|
|
312
|
+
* every such question and throws NeedsInputError only for the others. If they answer first, yours is dropped.
|
|
313
|
+
*/
|
|
314
|
+
onQuestion?: (question: Question, task: Task) => string | null | undefined | Promise<string | null | undefined>;
|
|
315
|
+
/**
|
|
316
|
+
* The agent wants to do something consequential: return true (or 'approve') to let it, false (or 'deny', or
|
|
317
|
+
* { decision, note }) not to. When the person at the computer can decide it on their screen
|
|
318
|
+
* (approval.customer_can_approve), return null to leave it to them; without onApproval, run() does that for
|
|
319
|
+
* every such approval and throws NeedsInputError only for the others. If they decide first, yours is dropped.
|
|
320
|
+
*/
|
|
321
|
+
onApproval?: (approval: Approval, task: Task) => ApprovalReply | Promise<ApprovalReply>;
|
|
277
322
|
/** Give up (and stop the task) after this long, in ms. */
|
|
278
323
|
timeout?: number;
|
|
279
324
|
};
|
|
@@ -305,7 +350,7 @@ declare class Tasks extends Resource {
|
|
|
305
350
|
stream(taskId: string, { after }?: {
|
|
306
351
|
after?: number;
|
|
307
352
|
}): AsyncGenerator<TaskEvent, Task>;
|
|
308
|
-
/** Starts a task and sees it through: events to onEvent, questions to onQuestion
|
|
353
|
+
/** Starts a task and sees it through: events to onEvent, questions to onQuestion and approvals to onApproval (or the person at the computer). Resolves with the finished task. */
|
|
309
354
|
run(sessionId: string, { onEvent, onQuestion, onApproval, timeout, ...params }: RunOptions): Promise<Task>;
|
|
310
355
|
}
|
|
311
356
|
declare class WebhookEndpoint extends Resource {
|
package/dist/cjs/index.js
CHANGED
|
@@ -8,10 +8,11 @@
|
|
|
8
8
|
// // send session.invite_url to the person at the computer, then:
|
|
9
9
|
// await gh.sessions.waitForConnection(session.session_id);
|
|
10
10
|
// const task = await gh.tasks.run(session.session_id, { prompt: 'Turn on Dark Mode', onQuestion: async (q) => 'Work', onApproval: async () => true });
|
|
11
|
+
// // (without onQuestion or onApproval, what the person at the computer can answer or decide on their screen is left to them)
|
|
11
12
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
13
|
exports.GuidingHand = exports.WebhookVerificationError = exports.NeedsInputError = exports.SessionExpiredError = exports.TimeoutError = exports.APIConnectionError = exports.APIError = exports.RateLimitError = exports.ConflictError = exports.NotFoundError = exports.PermissionDeniedError = exports.PaymentRequiredError = exports.AuthenticationError = exports.InvalidRequestError = exports.GuidingHandError = exports.VERSION = void 0;
|
|
13
14
|
exports.verifyWebhook = verifyWebhook;
|
|
14
|
-
exports.VERSION = '0.
|
|
15
|
+
exports.VERSION = '0.3.0';
|
|
15
16
|
// ---------- errors ----------
|
|
16
17
|
class GuidingHandError extends Error {
|
|
17
18
|
status;
|
|
@@ -44,6 +45,7 @@ exports.PermissionDeniedError = PermissionDeniedError;
|
|
|
44
45
|
class NotFoundError extends GuidingHandError {
|
|
45
46
|
}
|
|
46
47
|
exports.NotFoundError = NotFoundError;
|
|
48
|
+
/** 409. From respond(): extra.code is 'already_answered' when someone answered or decided first (extra.answered_by: 'customer' is the person at the computer), else 'not_pending'. */
|
|
47
49
|
class ConflictError extends GuidingHandError {
|
|
48
50
|
}
|
|
49
51
|
exports.ConflictError = ConflictError;
|
|
@@ -62,7 +64,7 @@ exports.TimeoutError = TimeoutError;
|
|
|
62
64
|
class SessionExpiredError extends GuidingHandError {
|
|
63
65
|
}
|
|
64
66
|
exports.SessionExpiredError = SessionExpiredError;
|
|
65
|
-
/** A task waits on a question or approval and `run()` got no handler for it. */
|
|
67
|
+
/** A task waits on a question or approval and `run()` got no handler for it (one the person at the computer can answer or decide is left to them instead). */
|
|
66
68
|
class NeedsInputError extends GuidingHandError {
|
|
67
69
|
task;
|
|
68
70
|
constructor(task) { super(`The task is waiting for ${task.pending?.type === 'approval' ? 'an approval' : 'an answer'}; pass ${task.pending?.type === 'approval' ? 'onApproval' : 'onQuestion'} to run().`, 0, 'needs_input'); this.task = task; }
|
|
@@ -212,7 +214,7 @@ class Tasks extends Resource {
|
|
|
212
214
|
return r.task;
|
|
213
215
|
}
|
|
214
216
|
}
|
|
215
|
-
/** Starts a task and sees it through: events to onEvent, questions to onQuestion
|
|
217
|
+
/** Starts a task and sees it through: events to onEvent, questions to onQuestion and approvals to onApproval (or the person at the computer). Resolves with the finished task. */
|
|
216
218
|
async run(sessionId, { onEvent, onQuestion, onApproval, timeout, ...params }) {
|
|
217
219
|
let task = await this.create(sessionId, params);
|
|
218
220
|
const until = timeout ? Date.now() + timeout : Infinity;
|
|
@@ -234,23 +236,35 @@ class Tasks extends Resource {
|
|
|
234
236
|
continue;
|
|
235
237
|
const id = p.type === 'question' ? p.question_id : p.approval_id;
|
|
236
238
|
if (id === answered)
|
|
237
|
-
continue; // answered already; the task is picking it up
|
|
239
|
+
continue; // answered or decided already (or left to the person at the computer); the task is picking it up
|
|
238
240
|
try {
|
|
239
241
|
if (p.type === 'question') {
|
|
240
|
-
|
|
242
|
+
// Without onQuestion, or when it returns nothing, a question the person at the computer can answer on
|
|
243
|
+
// their screen is left to them: keep following.
|
|
244
|
+
if (!onQuestion && !p.customer_can_answer)
|
|
241
245
|
throw new NeedsInputError(task);
|
|
242
|
-
|
|
246
|
+
const reply = onQuestion ? await onQuestion(p, task) : null;
|
|
247
|
+
if (reply !== null && reply !== undefined)
|
|
248
|
+
await this.respond(task.task_id, { question_id: p.question_id, answer: String(reply) });
|
|
249
|
+
else if (!p.customer_can_answer)
|
|
250
|
+
throw new TypeError('onQuestion returned no answer, and the person at the computer can’t answer this question (customer_can_answer is false).');
|
|
243
251
|
}
|
|
244
252
|
else {
|
|
245
|
-
|
|
253
|
+
// The same for an approval the person at the computer can decide on their screen.
|
|
254
|
+
if (!onApproval && !p.customer_can_approve)
|
|
246
255
|
throw new NeedsInputError(task);
|
|
247
|
-
const d = await onApproval(p, task);
|
|
248
|
-
|
|
249
|
-
|
|
256
|
+
const d = onApproval ? await onApproval(p, task) : null;
|
|
257
|
+
// null leaves it to the person at the computer when they can decide it; otherwise it denies, as before.
|
|
258
|
+
if ((d === null || d === undefined) && p.customer_can_approve) { /* theirs */ }
|
|
259
|
+
else {
|
|
260
|
+
const decision = d && typeof d === 'object' ? d : { decision: d === true || d === 'approve' ? 'approve' : 'deny' };
|
|
261
|
+
await this.respond(task.task_id, { approval_id: p.approval_id, ...decision });
|
|
262
|
+
}
|
|
250
263
|
}
|
|
251
264
|
}
|
|
252
265
|
catch (e) {
|
|
253
|
-
// Answered
|
|
266
|
+
// Answered or decided somewhere else first (the person at the computer, the console, another process: extra.code
|
|
267
|
+
// 'already_answered') or the task ended meanwhile: keep following.
|
|
254
268
|
if (!(e instanceof ConflictError))
|
|
255
269
|
throw e;
|
|
256
270
|
}
|
package/dist/esm/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const VERSION = "0.
|
|
1
|
+
export declare const VERSION = "0.3.0";
|
|
2
2
|
export type Metadata = Record<string, string>;
|
|
3
3
|
export type Effort = 'low' | 'medium' | 'high' | null;
|
|
4
4
|
export type Agent = {
|
|
@@ -8,6 +8,14 @@ export type Agent = {
|
|
|
8
8
|
instructions: string;
|
|
9
9
|
effort: Effort;
|
|
10
10
|
greeting: string;
|
|
11
|
+
/** What the app calls the agent on the person's screen; '' is "GuidingHand". */
|
|
12
|
+
display_name: string;
|
|
13
|
+
/** The app shows the agent's thoughts and steps on the person's screen as it works. */
|
|
14
|
+
narration: boolean;
|
|
15
|
+
/** The person at the computer can answer the agent's questions in the app (the team still can; the first answer is used). */
|
|
16
|
+
customer_answers: boolean;
|
|
17
|
+
/** The person at the computer can approve or deny the agent's approval requests in the app (the team still can; the first decision is used). */
|
|
18
|
+
customer_approvals: boolean;
|
|
11
19
|
is_default: boolean;
|
|
12
20
|
invite_url_template: string;
|
|
13
21
|
created_at: string | null;
|
|
@@ -38,19 +46,29 @@ export type Session = {
|
|
|
38
46
|
latest_task_id?: string | null;
|
|
39
47
|
};
|
|
40
48
|
export type TaskStatus = 'queued' | 'running' | 'waiting_for_user' | 'waiting_for_approval' | 'completed' | 'failed' | 'stopped';
|
|
49
|
+
/** customer_can_answer: the person at the computer can answer it on their screen too (the first answer is used). */
|
|
41
50
|
export type Question = {
|
|
42
51
|
type: 'question';
|
|
43
52
|
question_id: string;
|
|
44
53
|
question: string;
|
|
45
54
|
options: string[];
|
|
55
|
+
customer_can_answer: boolean;
|
|
46
56
|
};
|
|
57
|
+
/** customer_can_approve: the person at the computer can approve or deny it on their screen too (the first decision is used). */
|
|
47
58
|
export type Approval = {
|
|
48
59
|
type: 'approval';
|
|
49
60
|
approval_id: string;
|
|
50
61
|
action: string;
|
|
51
62
|
risk: 'low' | 'medium' | 'high';
|
|
63
|
+
customer_can_approve: boolean;
|
|
52
64
|
};
|
|
53
65
|
export type EventType = 'started' | 'progress' | 'thinking' | 'action' | 'message' | 'question' | 'answer' | 'approval_required' | 'approved' | 'denied' | 'completed' | 'error' | 'stopped';
|
|
66
|
+
/** Who answered a question or decided an approval: the person at the computer ('customer', in the app) or your team ('operator': the API or the console). */
|
|
67
|
+
export type AnsweredBy = 'customer' | 'operator';
|
|
68
|
+
/**
|
|
69
|
+
* data: e.g. { action } for 'action', { question_id, question, options } for 'question', { question_id, answered_by } for 'answer'
|
|
70
|
+
* (whose message is the answer), { approval_id, answered_by, note? } for 'approved' and 'denied'.
|
|
71
|
+
*/
|
|
54
72
|
export type TaskEvent = {
|
|
55
73
|
cursor: number;
|
|
56
74
|
type: EventType;
|
|
@@ -96,7 +114,7 @@ export type Recording = {
|
|
|
96
114
|
task_id: string;
|
|
97
115
|
frames: Frame[];
|
|
98
116
|
};
|
|
99
|
-
export type WebhookEventType = 'session.connected' | 'session.disconnected' | 'task.started' | 'task.waiting_for_user' | 'task.waiting_for_approval' | 'task.completed' | 'task.failed' | 'task.stopped';
|
|
117
|
+
export type WebhookEventType = 'session.connected' | 'session.disconnected' | 'task.started' | 'task.waiting_for_user' | 'task.question_answered' | 'task.waiting_for_approval' | 'task.approval_decided' | 'task.completed' | 'task.failed' | 'task.stopped';
|
|
100
118
|
export type Webhook = {
|
|
101
119
|
object: 'webhook';
|
|
102
120
|
url: string | null;
|
|
@@ -105,6 +123,18 @@ export type Webhook = {
|
|
|
105
123
|
secret?: string;
|
|
106
124
|
event_types: WebhookEventType[];
|
|
107
125
|
};
|
|
126
|
+
export type QuestionAnswer = {
|
|
127
|
+
question_id: string;
|
|
128
|
+
answer: string;
|
|
129
|
+
answered_by: AnsweredBy;
|
|
130
|
+
};
|
|
131
|
+
export type ApprovalDecision = {
|
|
132
|
+
approval_id: string;
|
|
133
|
+
decision: 'approve' | 'deny';
|
|
134
|
+
note: string | null;
|
|
135
|
+
answered_by: AnsweredBy;
|
|
136
|
+
};
|
|
137
|
+
/** data.answer: for task.question_answered. data.decision: for task.approval_decided. */
|
|
108
138
|
export type WebhookEvent = {
|
|
109
139
|
id: string;
|
|
110
140
|
type: WebhookEventType;
|
|
@@ -113,6 +143,8 @@ export type WebhookEvent = {
|
|
|
113
143
|
data: {
|
|
114
144
|
task?: Task;
|
|
115
145
|
session?: Session;
|
|
146
|
+
answer?: QuestionAnswer;
|
|
147
|
+
decision?: ApprovalDecision;
|
|
116
148
|
};
|
|
117
149
|
};
|
|
118
150
|
export type Page<T> = {
|
|
@@ -141,6 +173,7 @@ export declare class PermissionDeniedError extends GuidingHandError {
|
|
|
141
173
|
}
|
|
142
174
|
export declare class NotFoundError extends GuidingHandError {
|
|
143
175
|
}
|
|
176
|
+
/** 409. From respond(): extra.code is 'already_answered' when someone answered or decided first (extra.answered_by: 'customer' is the person at the computer), else 'not_pending'. */
|
|
144
177
|
export declare class ConflictError extends GuidingHandError {
|
|
145
178
|
}
|
|
146
179
|
export declare class RateLimitError extends GuidingHandError {
|
|
@@ -153,7 +186,7 @@ export declare class TimeoutError extends GuidingHandError {
|
|
|
153
186
|
}
|
|
154
187
|
export declare class SessionExpiredError extends GuidingHandError {
|
|
155
188
|
}
|
|
156
|
-
/** A task waits on a question or approval and `run()` got no handler for it. */
|
|
189
|
+
/** A task waits on a question or approval and `run()` got no handler for it (one the person at the computer can answer or decide is left to them instead). */
|
|
157
190
|
export declare class NeedsInputError extends GuidingHandError {
|
|
158
191
|
task: Task;
|
|
159
192
|
constructor(task: Task);
|
|
@@ -205,6 +238,10 @@ export type AgentCreate = {
|
|
|
205
238
|
instructions?: string;
|
|
206
239
|
effort?: Effort;
|
|
207
240
|
greeting?: string;
|
|
241
|
+
display_name?: string;
|
|
242
|
+
narration?: boolean;
|
|
243
|
+
customer_answers?: boolean;
|
|
244
|
+
customer_approvals?: boolean;
|
|
208
245
|
};
|
|
209
246
|
export type AgentUpdate = Partial<Omit<AgentCreate, 'agent_id'>>;
|
|
210
247
|
declare class Agents extends Resource {
|
|
@@ -261,19 +298,27 @@ export type Respond = {
|
|
|
261
298
|
decision: 'approve' | 'deny';
|
|
262
299
|
note?: string;
|
|
263
300
|
};
|
|
301
|
+
/** null leaves the approval to the person at the computer when they can decide it (customer_can_approve); otherwise it denies. */
|
|
302
|
+
export type ApprovalReply = boolean | 'approve' | 'deny' | {
|
|
303
|
+
decision: 'approve' | 'deny';
|
|
304
|
+
note?: string;
|
|
305
|
+
} | null | undefined;
|
|
264
306
|
export type RunOptions = TaskCreate & {
|
|
265
307
|
/** Every event, as it happens. */
|
|
266
308
|
onEvent?: (event: TaskEvent, task: Task) => void | Promise<void>;
|
|
267
|
-
/**
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
309
|
+
/**
|
|
310
|
+
* The agent asks something: return the answer. When the person at the computer can answer it on their screen
|
|
311
|
+
* (question.customer_can_answer), return null to leave it to them; without onQuestion, run() does that for
|
|
312
|
+
* every such question and throws NeedsInputError only for the others. If they answer first, yours is dropped.
|
|
313
|
+
*/
|
|
314
|
+
onQuestion?: (question: Question, task: Task) => string | null | undefined | Promise<string | null | undefined>;
|
|
315
|
+
/**
|
|
316
|
+
* The agent wants to do something consequential: return true (or 'approve') to let it, false (or 'deny', or
|
|
317
|
+
* { decision, note }) not to. When the person at the computer can decide it on their screen
|
|
318
|
+
* (approval.customer_can_approve), return null to leave it to them; without onApproval, run() does that for
|
|
319
|
+
* every such approval and throws NeedsInputError only for the others. If they decide first, yours is dropped.
|
|
320
|
+
*/
|
|
321
|
+
onApproval?: (approval: Approval, task: Task) => ApprovalReply | Promise<ApprovalReply>;
|
|
277
322
|
/** Give up (and stop the task) after this long, in ms. */
|
|
278
323
|
timeout?: number;
|
|
279
324
|
};
|
|
@@ -305,7 +350,7 @@ declare class Tasks extends Resource {
|
|
|
305
350
|
stream(taskId: string, { after }?: {
|
|
306
351
|
after?: number;
|
|
307
352
|
}): AsyncGenerator<TaskEvent, Task>;
|
|
308
|
-
/** Starts a task and sees it through: events to onEvent, questions to onQuestion
|
|
353
|
+
/** Starts a task and sees it through: events to onEvent, questions to onQuestion and approvals to onApproval (or the person at the computer). Resolves with the finished task. */
|
|
309
354
|
run(sessionId: string, { onEvent, onQuestion, onApproval, timeout, ...params }: RunOptions): Promise<Task>;
|
|
310
355
|
}
|
|
311
356
|
declare class WebhookEndpoint extends Resource {
|
package/dist/esm/index.js
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
// // send session.invite_url to the person at the computer, then:
|
|
8
8
|
// await gh.sessions.waitForConnection(session.session_id);
|
|
9
9
|
// const task = await gh.tasks.run(session.session_id, { prompt: 'Turn on Dark Mode', onQuestion: async (q) => 'Work', onApproval: async () => true });
|
|
10
|
-
|
|
10
|
+
// // (without onQuestion or onApproval, what the person at the computer can answer or decide on their screen is left to them)
|
|
11
|
+
export const VERSION = '0.3.0';
|
|
11
12
|
// ---------- errors ----------
|
|
12
13
|
export class GuidingHandError extends Error {
|
|
13
14
|
status;
|
|
@@ -34,6 +35,7 @@ export class PermissionDeniedError extends GuidingHandError {
|
|
|
34
35
|
}
|
|
35
36
|
export class NotFoundError extends GuidingHandError {
|
|
36
37
|
}
|
|
38
|
+
/** 409. From respond(): extra.code is 'already_answered' when someone answered or decided first (extra.answered_by: 'customer' is the person at the computer), else 'not_pending'. */
|
|
37
39
|
export class ConflictError extends GuidingHandError {
|
|
38
40
|
}
|
|
39
41
|
export class RateLimitError extends GuidingHandError {
|
|
@@ -46,7 +48,7 @@ export class TimeoutError extends GuidingHandError {
|
|
|
46
48
|
}
|
|
47
49
|
export class SessionExpiredError extends GuidingHandError {
|
|
48
50
|
}
|
|
49
|
-
/** A task waits on a question or approval and `run()` got no handler for it. */
|
|
51
|
+
/** A task waits on a question or approval and `run()` got no handler for it (one the person at the computer can answer or decide is left to them instead). */
|
|
50
52
|
export class NeedsInputError extends GuidingHandError {
|
|
51
53
|
task;
|
|
52
54
|
constructor(task) { super(`The task is waiting for ${task.pending?.type === 'approval' ? 'an approval' : 'an answer'}; pass ${task.pending?.type === 'approval' ? 'onApproval' : 'onQuestion'} to run().`, 0, 'needs_input'); this.task = task; }
|
|
@@ -193,7 +195,7 @@ class Tasks extends Resource {
|
|
|
193
195
|
return r.task;
|
|
194
196
|
}
|
|
195
197
|
}
|
|
196
|
-
/** Starts a task and sees it through: events to onEvent, questions to onQuestion
|
|
198
|
+
/** Starts a task and sees it through: events to onEvent, questions to onQuestion and approvals to onApproval (or the person at the computer). Resolves with the finished task. */
|
|
197
199
|
async run(sessionId, { onEvent, onQuestion, onApproval, timeout, ...params }) {
|
|
198
200
|
let task = await this.create(sessionId, params);
|
|
199
201
|
const until = timeout ? Date.now() + timeout : Infinity;
|
|
@@ -215,23 +217,35 @@ class Tasks extends Resource {
|
|
|
215
217
|
continue;
|
|
216
218
|
const id = p.type === 'question' ? p.question_id : p.approval_id;
|
|
217
219
|
if (id === answered)
|
|
218
|
-
continue; // answered already; the task is picking it up
|
|
220
|
+
continue; // answered or decided already (or left to the person at the computer); the task is picking it up
|
|
219
221
|
try {
|
|
220
222
|
if (p.type === 'question') {
|
|
221
|
-
|
|
223
|
+
// Without onQuestion, or when it returns nothing, a question the person at the computer can answer on
|
|
224
|
+
// their screen is left to them: keep following.
|
|
225
|
+
if (!onQuestion && !p.customer_can_answer)
|
|
222
226
|
throw new NeedsInputError(task);
|
|
223
|
-
|
|
227
|
+
const reply = onQuestion ? await onQuestion(p, task) : null;
|
|
228
|
+
if (reply !== null && reply !== undefined)
|
|
229
|
+
await this.respond(task.task_id, { question_id: p.question_id, answer: String(reply) });
|
|
230
|
+
else if (!p.customer_can_answer)
|
|
231
|
+
throw new TypeError('onQuestion returned no answer, and the person at the computer can’t answer this question (customer_can_answer is false).');
|
|
224
232
|
}
|
|
225
233
|
else {
|
|
226
|
-
|
|
234
|
+
// The same for an approval the person at the computer can decide on their screen.
|
|
235
|
+
if (!onApproval && !p.customer_can_approve)
|
|
227
236
|
throw new NeedsInputError(task);
|
|
228
|
-
const d = await onApproval(p, task);
|
|
229
|
-
|
|
230
|
-
|
|
237
|
+
const d = onApproval ? await onApproval(p, task) : null;
|
|
238
|
+
// null leaves it to the person at the computer when they can decide it; otherwise it denies, as before.
|
|
239
|
+
if ((d === null || d === undefined) && p.customer_can_approve) { /* theirs */ }
|
|
240
|
+
else {
|
|
241
|
+
const decision = d && typeof d === 'object' ? d : { decision: d === true || d === 'approve' ? 'approve' : 'deny' };
|
|
242
|
+
await this.respond(task.task_id, { approval_id: p.approval_id, ...decision });
|
|
243
|
+
}
|
|
231
244
|
}
|
|
232
245
|
}
|
|
233
246
|
catch (e) {
|
|
234
|
-
// Answered
|
|
247
|
+
// Answered or decided somewhere else first (the person at the computer, the console, another process: extra.code
|
|
248
|
+
// 'already_answered') or the task ended meanwhile: keep following.
|
|
235
249
|
if (!(e instanceof ConflictError))
|
|
236
250
|
throw e;
|
|
237
251
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "guidinghand",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "GuidingHand SDK: put an AI agent on your customer's computer with one link. Sessions, tasks, agents, recordings and webhooks.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://guidinghand.ai",
|