guidinghand 0.2.0 → 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 +14 -9
- package/dist/cjs/index.d.ts +36 -17
- package/dist/cjs/index.js +16 -11
- package/dist/esm/index.d.ts +36 -17
- package/dist/esm/index.js +16 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -27,7 +27,7 @@ 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
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'
|
|
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."
|
|
@@ -41,7 +41,7 @@ An agent is your configuration: instructions (added under GuidingHand's own rule
|
|
|
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 }); // on their screen
|
|
44
|
+
await gh.agents.update('billing', { display_name: 'Acme Support', narration: true, customer_answers: false, customer_approvals: false }); // on their screen
|
|
45
45
|
const { data } = await gh.agents.list();
|
|
46
46
|
await gh.agents.delete('billing');
|
|
47
47
|
```
|
|
@@ -73,7 +73,7 @@ const page = await gh.tasks.list({ status: 'completed', limit: 50 }); // { da
|
|
|
73
73
|
for await (const t of gh.tasks.listAll({ agent_id: 'billing' })) { /* every page */ }
|
|
74
74
|
```
|
|
75
75
|
|
|
76
|
-
## Questions the customer answers
|
|
76
|
+
## Questions and approvals the customer answers
|
|
77
77
|
|
|
78
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
79
|
|
|
@@ -82,17 +82,21 @@ Unless the agent turns it off (`customer_answers: false`), the agent's questions
|
|
|
82
82
|
- `respond()` after they answered throws `ConflictError` with `extra.code === 'already_answered'` and `extra.answered_by === 'customer'`.
|
|
83
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
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
|
+
|
|
85
91
|
```ts
|
|
86
92
|
const task = await gh.tasks.run(sessionId, {
|
|
87
93
|
prompt: 'Set up the office printer',
|
|
88
94
|
onEvent: (e) => { if (e.type === 'answer') console.log(`${e.data?.answered_by} answered: ${e.message}`); },
|
|
89
95
|
onQuestion: async (q) => (q.customer_can_answer ? null : askMyTeam(q.question, q.options)),
|
|
90
|
-
onApproval: async (a) => askMyTeamToApprove(a.action),
|
|
96
|
+
onApproval: async (a) => (a.customer_can_approve && a.risk !== 'high' ? null : askMyTeamToApprove(a.action)),
|
|
91
97
|
});
|
|
92
98
|
```
|
|
93
99
|
|
|
94
|
-
Approvals always come from your team.
|
|
95
|
-
|
|
96
100
|
## Recordings
|
|
97
101
|
|
|
98
102
|
```ts
|
|
@@ -105,7 +109,7 @@ The console replays them with the agent's cursor at `task.replay_url`.
|
|
|
105
109
|
## Webhooks
|
|
106
110
|
|
|
107
111
|
```ts
|
|
108
|
-
const { secret } = await gh.webhook.update({ url: 'https://example.com/guidinghand', events: ['task.completed', 'task.waiting_for_user', 'task.question_answered'] });
|
|
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'] });
|
|
109
113
|
```
|
|
110
114
|
|
|
111
115
|
Verify each delivery with the raw request body:
|
|
@@ -117,6 +121,7 @@ app.post('/guidinghand', express.raw({ type: 'application/json' }), async (req,
|
|
|
117
121
|
const event = await verifyWebhook(req.body, req.header('GuidingHand-Signature'), process.env.GUIDINGHAND_WEBHOOK_SECRET);
|
|
118
122
|
if (event.type === 'task.completed') console.log(event.data.task.result);
|
|
119
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);
|
|
120
125
|
res.sendStatus(200);
|
|
121
126
|
});
|
|
122
127
|
```
|
|
@@ -132,11 +137,11 @@ Every error is a `GuidingHandError` with `status`, `type`, `message` and `extra`
|
|
|
132
137
|
| `PaymentRequiredError` | 402 | the org's plan doesn't allow it (free minutes used up) |
|
|
133
138
|
| `PermissionDeniedError` | 403 | your role can't do this |
|
|
134
139
|
| `NotFoundError` | 404 | not in this org |
|
|
135
|
-
| `ConflictError` | 409 | no computer connected, a task already running (`extra.active_task_id`), nothing pending, or already answered (`extra.code: 'already_answered'`, `extra.answered_by`) |
|
|
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`) |
|
|
136
141
|
| `RateLimitError` | 429 | too many requests, or the plan's tasks at once |
|
|
137
142
|
| `APIError` | 5xx | our side |
|
|
138
143
|
| `APIConnectionError`, `TimeoutError` | | network |
|
|
139
|
-
| `NeedsInputError` | | `run()` got a question (that the customer can't answer
|
|
144
|
+
| `NeedsInputError` | | `run()` got a question or approval (that the customer can't answer or decide) without a handler (`error.task`) |
|
|
140
145
|
| `SessionExpiredError` | | `waitForConnection()` on a code that expired |
|
|
141
146
|
|
|
142
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 = {
|
|
@@ -12,8 +12,10 @@ export type Agent = {
|
|
|
12
12
|
display_name: string;
|
|
13
13
|
/** The app shows the agent's thoughts and steps on the person's screen as it works. */
|
|
14
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).
|
|
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
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;
|
|
17
19
|
is_default: boolean;
|
|
18
20
|
invite_url_template: string;
|
|
19
21
|
created_at: string | null;
|
|
@@ -52,16 +54,21 @@ export type Question = {
|
|
|
52
54
|
options: string[];
|
|
53
55
|
customer_can_answer: boolean;
|
|
54
56
|
};
|
|
57
|
+
/** customer_can_approve: the person at the computer can approve or deny it on their screen too (the first decision is used). */
|
|
55
58
|
export type Approval = {
|
|
56
59
|
type: 'approval';
|
|
57
60
|
approval_id: string;
|
|
58
61
|
action: string;
|
|
59
62
|
risk: 'low' | 'medium' | 'high';
|
|
63
|
+
customer_can_approve: boolean;
|
|
60
64
|
};
|
|
61
65
|
export type EventType = 'started' | 'progress' | 'thinking' | 'action' | 'message' | 'question' | 'answer' | 'approval_required' | 'approved' | 'denied' | 'completed' | 'error' | 'stopped';
|
|
62
|
-
/** Who answered a question: the person at the computer ('customer', in the app) or your team ('operator': the API or the console). */
|
|
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). */
|
|
63
67
|
export type AnsweredBy = 'customer' | 'operator';
|
|
64
|
-
/**
|
|
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
|
+
*/
|
|
65
72
|
export type TaskEvent = {
|
|
66
73
|
cursor: number;
|
|
67
74
|
type: EventType;
|
|
@@ -107,7 +114,7 @@ export type Recording = {
|
|
|
107
114
|
task_id: string;
|
|
108
115
|
frames: Frame[];
|
|
109
116
|
};
|
|
110
|
-
export type WebhookEventType = 'session.connected' | 'session.disconnected' | 'task.started' | 'task.waiting_for_user' | 'task.question_answered' | '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';
|
|
111
118
|
export type Webhook = {
|
|
112
119
|
object: 'webhook';
|
|
113
120
|
url: string | null;
|
|
@@ -121,7 +128,13 @@ export type QuestionAnswer = {
|
|
|
121
128
|
answer: string;
|
|
122
129
|
answered_by: AnsweredBy;
|
|
123
130
|
};
|
|
124
|
-
|
|
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. */
|
|
125
138
|
export type WebhookEvent = {
|
|
126
139
|
id: string;
|
|
127
140
|
type: WebhookEventType;
|
|
@@ -131,6 +144,7 @@ export type WebhookEvent = {
|
|
|
131
144
|
task?: Task;
|
|
132
145
|
session?: Session;
|
|
133
146
|
answer?: QuestionAnswer;
|
|
147
|
+
decision?: ApprovalDecision;
|
|
134
148
|
};
|
|
135
149
|
};
|
|
136
150
|
export type Page<T> = {
|
|
@@ -159,7 +173,7 @@ export declare class PermissionDeniedError extends GuidingHandError {
|
|
|
159
173
|
}
|
|
160
174
|
export declare class NotFoundError extends GuidingHandError {
|
|
161
175
|
}
|
|
162
|
-
/** 409. From respond(): extra.code is 'already_answered' when someone answered first (extra.answered_by: 'customer' is the person at the computer), else 'not_pending'. */
|
|
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'. */
|
|
163
177
|
export declare class ConflictError extends GuidingHandError {
|
|
164
178
|
}
|
|
165
179
|
export declare class RateLimitError extends GuidingHandError {
|
|
@@ -172,7 +186,7 @@ export declare class TimeoutError extends GuidingHandError {
|
|
|
172
186
|
}
|
|
173
187
|
export declare class SessionExpiredError extends GuidingHandError {
|
|
174
188
|
}
|
|
175
|
-
/** 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). */
|
|
176
190
|
export declare class NeedsInputError extends GuidingHandError {
|
|
177
191
|
task: Task;
|
|
178
192
|
constructor(task: Task);
|
|
@@ -227,6 +241,7 @@ export type AgentCreate = {
|
|
|
227
241
|
display_name?: string;
|
|
228
242
|
narration?: boolean;
|
|
229
243
|
customer_answers?: boolean;
|
|
244
|
+
customer_approvals?: boolean;
|
|
230
245
|
};
|
|
231
246
|
export type AgentUpdate = Partial<Omit<AgentCreate, 'agent_id'>>;
|
|
232
247
|
declare class Agents extends Resource {
|
|
@@ -283,6 +298,11 @@ export type Respond = {
|
|
|
283
298
|
decision: 'approve' | 'deny';
|
|
284
299
|
note?: string;
|
|
285
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;
|
|
286
306
|
export type RunOptions = TaskCreate & {
|
|
287
307
|
/** Every event, as it happens. */
|
|
288
308
|
onEvent?: (event: TaskEvent, task: Task) => void | Promise<void>;
|
|
@@ -292,14 +312,13 @@ export type RunOptions = TaskCreate & {
|
|
|
292
312
|
* every such question and throws NeedsInputError only for the others. If they answer first, yours is dropped.
|
|
293
313
|
*/
|
|
294
314
|
onQuestion?: (question: Question, task: Task) => string | null | undefined | Promise<string | null | undefined>;
|
|
295
|
-
/**
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
}>;
|
|
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>;
|
|
303
322
|
/** Give up (and stop the task) after this long, in ms. */
|
|
304
323
|
timeout?: number;
|
|
305
324
|
};
|
|
@@ -331,7 +350,7 @@ declare class Tasks extends Resource {
|
|
|
331
350
|
stream(taskId: string, { after }?: {
|
|
332
351
|
after?: number;
|
|
333
352
|
}): AsyncGenerator<TaskEvent, Task>;
|
|
334
|
-
/** Starts a task and sees it through: events to onEvent, questions to onQuestion (or the person at the computer)
|
|
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. */
|
|
335
354
|
run(sessionId: string, { onEvent, onQuestion, onApproval, timeout, ...params }: RunOptions): Promise<Task>;
|
|
336
355
|
}
|
|
337
356
|
declare class WebhookEndpoint extends Resource {
|
package/dist/cjs/index.js
CHANGED
|
@@ -8,11 +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,
|
|
11
|
+
// // (without onQuestion or onApproval, what the person at the computer can answer or decide on their screen is left to them)
|
|
12
12
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
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;
|
|
14
14
|
exports.verifyWebhook = verifyWebhook;
|
|
15
|
-
exports.VERSION = '0.
|
|
15
|
+
exports.VERSION = '0.3.0';
|
|
16
16
|
// ---------- errors ----------
|
|
17
17
|
class GuidingHandError extends Error {
|
|
18
18
|
status;
|
|
@@ -45,7 +45,7 @@ exports.PermissionDeniedError = PermissionDeniedError;
|
|
|
45
45
|
class NotFoundError extends GuidingHandError {
|
|
46
46
|
}
|
|
47
47
|
exports.NotFoundError = NotFoundError;
|
|
48
|
-
/** 409. From respond(): extra.code is 'already_answered' when someone answered first (extra.answered_by: 'customer' is the person at the computer), else 'not_pending'. */
|
|
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'. */
|
|
49
49
|
class ConflictError extends GuidingHandError {
|
|
50
50
|
}
|
|
51
51
|
exports.ConflictError = ConflictError;
|
|
@@ -64,7 +64,7 @@ exports.TimeoutError = TimeoutError;
|
|
|
64
64
|
class SessionExpiredError extends GuidingHandError {
|
|
65
65
|
}
|
|
66
66
|
exports.SessionExpiredError = SessionExpiredError;
|
|
67
|
-
/** 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). */
|
|
68
68
|
class NeedsInputError extends GuidingHandError {
|
|
69
69
|
task;
|
|
70
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; }
|
|
@@ -214,7 +214,7 @@ class Tasks extends Resource {
|
|
|
214
214
|
return r.task;
|
|
215
215
|
}
|
|
216
216
|
}
|
|
217
|
-
/** Starts a task and sees it through: events to onEvent, questions to onQuestion (or the person at the computer)
|
|
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. */
|
|
218
218
|
async run(sessionId, { onEvent, onQuestion, onApproval, timeout, ...params }) {
|
|
219
219
|
let task = await this.create(sessionId, params);
|
|
220
220
|
const until = timeout ? Date.now() + timeout : Infinity;
|
|
@@ -236,7 +236,7 @@ class Tasks extends Resource {
|
|
|
236
236
|
continue;
|
|
237
237
|
const id = p.type === 'question' ? p.question_id : p.approval_id;
|
|
238
238
|
if (id === answered)
|
|
239
|
-
continue; // answered already (or left to the person at the computer); 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
|
|
240
240
|
try {
|
|
241
241
|
if (p.type === 'question') {
|
|
242
242
|
// Without onQuestion, or when it returns nothing, a question the person at the computer can answer on
|
|
@@ -250,15 +250,20 @@ class Tasks extends Resource {
|
|
|
250
250
|
throw new TypeError('onQuestion returned no answer, and the person at the computer can’t answer this question (customer_can_answer is false).');
|
|
251
251
|
}
|
|
252
252
|
else {
|
|
253
|
-
|
|
253
|
+
// The same for an approval the person at the computer can decide on their screen.
|
|
254
|
+
if (!onApproval && !p.customer_can_approve)
|
|
254
255
|
throw new NeedsInputError(task);
|
|
255
|
-
const d = await onApproval(p, task);
|
|
256
|
-
|
|
257
|
-
|
|
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
|
+
}
|
|
258
263
|
}
|
|
259
264
|
}
|
|
260
265
|
catch (e) {
|
|
261
|
-
// Answered somewhere else first (the person at the computer, the console, another process: extra.code
|
|
266
|
+
// Answered or decided somewhere else first (the person at the computer, the console, another process: extra.code
|
|
262
267
|
// 'already_answered') or the task ended meanwhile: keep following.
|
|
263
268
|
if (!(e instanceof ConflictError))
|
|
264
269
|
throw e;
|
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 = {
|
|
@@ -12,8 +12,10 @@ export type Agent = {
|
|
|
12
12
|
display_name: string;
|
|
13
13
|
/** The app shows the agent's thoughts and steps on the person's screen as it works. */
|
|
14
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).
|
|
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
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;
|
|
17
19
|
is_default: boolean;
|
|
18
20
|
invite_url_template: string;
|
|
19
21
|
created_at: string | null;
|
|
@@ -52,16 +54,21 @@ export type Question = {
|
|
|
52
54
|
options: string[];
|
|
53
55
|
customer_can_answer: boolean;
|
|
54
56
|
};
|
|
57
|
+
/** customer_can_approve: the person at the computer can approve or deny it on their screen too (the first decision is used). */
|
|
55
58
|
export type Approval = {
|
|
56
59
|
type: 'approval';
|
|
57
60
|
approval_id: string;
|
|
58
61
|
action: string;
|
|
59
62
|
risk: 'low' | 'medium' | 'high';
|
|
63
|
+
customer_can_approve: boolean;
|
|
60
64
|
};
|
|
61
65
|
export type EventType = 'started' | 'progress' | 'thinking' | 'action' | 'message' | 'question' | 'answer' | 'approval_required' | 'approved' | 'denied' | 'completed' | 'error' | 'stopped';
|
|
62
|
-
/** Who answered a question: the person at the computer ('customer', in the app) or your team ('operator': the API or the console). */
|
|
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). */
|
|
63
67
|
export type AnsweredBy = 'customer' | 'operator';
|
|
64
|
-
/**
|
|
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
|
+
*/
|
|
65
72
|
export type TaskEvent = {
|
|
66
73
|
cursor: number;
|
|
67
74
|
type: EventType;
|
|
@@ -107,7 +114,7 @@ export type Recording = {
|
|
|
107
114
|
task_id: string;
|
|
108
115
|
frames: Frame[];
|
|
109
116
|
};
|
|
110
|
-
export type WebhookEventType = 'session.connected' | 'session.disconnected' | 'task.started' | 'task.waiting_for_user' | 'task.question_answered' | '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';
|
|
111
118
|
export type Webhook = {
|
|
112
119
|
object: 'webhook';
|
|
113
120
|
url: string | null;
|
|
@@ -121,7 +128,13 @@ export type QuestionAnswer = {
|
|
|
121
128
|
answer: string;
|
|
122
129
|
answered_by: AnsweredBy;
|
|
123
130
|
};
|
|
124
|
-
|
|
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. */
|
|
125
138
|
export type WebhookEvent = {
|
|
126
139
|
id: string;
|
|
127
140
|
type: WebhookEventType;
|
|
@@ -131,6 +144,7 @@ export type WebhookEvent = {
|
|
|
131
144
|
task?: Task;
|
|
132
145
|
session?: Session;
|
|
133
146
|
answer?: QuestionAnswer;
|
|
147
|
+
decision?: ApprovalDecision;
|
|
134
148
|
};
|
|
135
149
|
};
|
|
136
150
|
export type Page<T> = {
|
|
@@ -159,7 +173,7 @@ export declare class PermissionDeniedError extends GuidingHandError {
|
|
|
159
173
|
}
|
|
160
174
|
export declare class NotFoundError extends GuidingHandError {
|
|
161
175
|
}
|
|
162
|
-
/** 409. From respond(): extra.code is 'already_answered' when someone answered first (extra.answered_by: 'customer' is the person at the computer), else 'not_pending'. */
|
|
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'. */
|
|
163
177
|
export declare class ConflictError extends GuidingHandError {
|
|
164
178
|
}
|
|
165
179
|
export declare class RateLimitError extends GuidingHandError {
|
|
@@ -172,7 +186,7 @@ export declare class TimeoutError extends GuidingHandError {
|
|
|
172
186
|
}
|
|
173
187
|
export declare class SessionExpiredError extends GuidingHandError {
|
|
174
188
|
}
|
|
175
|
-
/** 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). */
|
|
176
190
|
export declare class NeedsInputError extends GuidingHandError {
|
|
177
191
|
task: Task;
|
|
178
192
|
constructor(task: Task);
|
|
@@ -227,6 +241,7 @@ export type AgentCreate = {
|
|
|
227
241
|
display_name?: string;
|
|
228
242
|
narration?: boolean;
|
|
229
243
|
customer_answers?: boolean;
|
|
244
|
+
customer_approvals?: boolean;
|
|
230
245
|
};
|
|
231
246
|
export type AgentUpdate = Partial<Omit<AgentCreate, 'agent_id'>>;
|
|
232
247
|
declare class Agents extends Resource {
|
|
@@ -283,6 +298,11 @@ export type Respond = {
|
|
|
283
298
|
decision: 'approve' | 'deny';
|
|
284
299
|
note?: string;
|
|
285
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;
|
|
286
306
|
export type RunOptions = TaskCreate & {
|
|
287
307
|
/** Every event, as it happens. */
|
|
288
308
|
onEvent?: (event: TaskEvent, task: Task) => void | Promise<void>;
|
|
@@ -292,14 +312,13 @@ export type RunOptions = TaskCreate & {
|
|
|
292
312
|
* every such question and throws NeedsInputError only for the others. If they answer first, yours is dropped.
|
|
293
313
|
*/
|
|
294
314
|
onQuestion?: (question: Question, task: Task) => string | null | undefined | Promise<string | null | undefined>;
|
|
295
|
-
/**
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
}>;
|
|
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>;
|
|
303
322
|
/** Give up (and stop the task) after this long, in ms. */
|
|
304
323
|
timeout?: number;
|
|
305
324
|
};
|
|
@@ -331,7 +350,7 @@ declare class Tasks extends Resource {
|
|
|
331
350
|
stream(taskId: string, { after }?: {
|
|
332
351
|
after?: number;
|
|
333
352
|
}): AsyncGenerator<TaskEvent, Task>;
|
|
334
|
-
/** Starts a task and sees it through: events to onEvent, questions to onQuestion (or the person at the computer)
|
|
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. */
|
|
335
354
|
run(sessionId: string, { onEvent, onQuestion, onApproval, timeout, ...params }: RunOptions): Promise<Task>;
|
|
336
355
|
}
|
|
337
356
|
declare class WebhookEndpoint extends Resource {
|
package/dist/esm/index.js
CHANGED
|
@@ -7,8 +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
|
-
// // (without onQuestion,
|
|
11
|
-
export const VERSION = '0.
|
|
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';
|
|
12
12
|
// ---------- errors ----------
|
|
13
13
|
export class GuidingHandError extends Error {
|
|
14
14
|
status;
|
|
@@ -35,7 +35,7 @@ export class PermissionDeniedError extends GuidingHandError {
|
|
|
35
35
|
}
|
|
36
36
|
export class NotFoundError extends GuidingHandError {
|
|
37
37
|
}
|
|
38
|
-
/** 409. From respond(): extra.code is 'already_answered' when someone answered first (extra.answered_by: 'customer' is the person at the computer), else 'not_pending'. */
|
|
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'. */
|
|
39
39
|
export class ConflictError extends GuidingHandError {
|
|
40
40
|
}
|
|
41
41
|
export class RateLimitError extends GuidingHandError {
|
|
@@ -48,7 +48,7 @@ export class TimeoutError extends GuidingHandError {
|
|
|
48
48
|
}
|
|
49
49
|
export class SessionExpiredError extends GuidingHandError {
|
|
50
50
|
}
|
|
51
|
-
/** 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). */
|
|
52
52
|
export class NeedsInputError extends GuidingHandError {
|
|
53
53
|
task;
|
|
54
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; }
|
|
@@ -195,7 +195,7 @@ class Tasks extends Resource {
|
|
|
195
195
|
return r.task;
|
|
196
196
|
}
|
|
197
197
|
}
|
|
198
|
-
/** Starts a task and sees it through: events to onEvent, questions to onQuestion (or the person at the computer)
|
|
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. */
|
|
199
199
|
async run(sessionId, { onEvent, onQuestion, onApproval, timeout, ...params }) {
|
|
200
200
|
let task = await this.create(sessionId, params);
|
|
201
201
|
const until = timeout ? Date.now() + timeout : Infinity;
|
|
@@ -217,7 +217,7 @@ class Tasks extends Resource {
|
|
|
217
217
|
continue;
|
|
218
218
|
const id = p.type === 'question' ? p.question_id : p.approval_id;
|
|
219
219
|
if (id === answered)
|
|
220
|
-
continue; // answered already (or left to the person at the computer); 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
|
|
221
221
|
try {
|
|
222
222
|
if (p.type === 'question') {
|
|
223
223
|
// Without onQuestion, or when it returns nothing, a question the person at the computer can answer on
|
|
@@ -231,15 +231,20 @@ class Tasks extends Resource {
|
|
|
231
231
|
throw new TypeError('onQuestion returned no answer, and the person at the computer can’t answer this question (customer_can_answer is false).');
|
|
232
232
|
}
|
|
233
233
|
else {
|
|
234
|
-
|
|
234
|
+
// The same for an approval the person at the computer can decide on their screen.
|
|
235
|
+
if (!onApproval && !p.customer_can_approve)
|
|
235
236
|
throw new NeedsInputError(task);
|
|
236
|
-
const d = await onApproval(p, task);
|
|
237
|
-
|
|
238
|
-
|
|
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
|
+
}
|
|
239
244
|
}
|
|
240
245
|
}
|
|
241
246
|
catch (e) {
|
|
242
|
-
// Answered somewhere else first (the person at the computer, the console, another process: extra.code
|
|
247
|
+
// Answered or decided somewhere else first (the person at the computer, the console, another process: extra.code
|
|
243
248
|
// 'already_answered') or the task ended meanwhile: keep following.
|
|
244
249
|
if (!(e instanceof ConflictError))
|
|
245
250
|
throw e;
|
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",
|