guidinghand 0.1.1 → 0.2.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 +27 -5
- package/dist/cjs/index.d.ts +32 -6
- package/dist/cjs/index.js +16 -7
- package/dist/esm/index.d.ts +32 -6
- package/dist/esm/index.js +16 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -26,7 +26,7 @@ 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
|
|
29
|
+
onQuestion: async (q) => 'Work', // q.question, q.options (leave it out to let the customer answer on their screen)
|
|
30
30
|
onApproval: async (a) => a.risk !== 'high', // true/'approve' or false/'deny'
|
|
31
31
|
});
|
|
32
32
|
|
|
@@ -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 }); // on their screen
|
|
44
45
|
const { data } = await gh.agents.list();
|
|
45
46
|
await gh.agents.delete('billing');
|
|
46
47
|
```
|
|
@@ -72,6 +73,26 @@ 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 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
|
+
```ts
|
|
86
|
+
const task = await gh.tasks.run(sessionId, {
|
|
87
|
+
prompt: 'Set up the office printer',
|
|
88
|
+
onEvent: (e) => { if (e.type === 'answer') console.log(`${e.data?.answered_by} answered: ${e.message}`); },
|
|
89
|
+
onQuestion: async (q) => (q.customer_can_answer ? null : askMyTeam(q.question, q.options)),
|
|
90
|
+
onApproval: async (a) => askMyTeamToApprove(a.action),
|
|
91
|
+
});
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Approvals always come from your team.
|
|
95
|
+
|
|
75
96
|
## Recordings
|
|
76
97
|
|
|
77
98
|
```ts
|
|
@@ -84,7 +105,7 @@ The console replays them with the agent's cursor at `task.replay_url`.
|
|
|
84
105
|
## Webhooks
|
|
85
106
|
|
|
86
107
|
```ts
|
|
87
|
-
const { secret } = await gh.webhook.update({ url: 'https://example.com/guidinghand', events: ['task.completed', 'task.waiting_for_user'] });
|
|
108
|
+
const { secret } = await gh.webhook.update({ url: 'https://example.com/guidinghand', events: ['task.completed', 'task.waiting_for_user', 'task.question_answered'] });
|
|
88
109
|
```
|
|
89
110
|
|
|
90
111
|
Verify each delivery with the raw request body:
|
|
@@ -95,6 +116,7 @@ import { verifyWebhook } from 'guidinghand';
|
|
|
95
116
|
app.post('/guidinghand', express.raw({ type: 'application/json' }), async (req, res) => {
|
|
96
117
|
const event = await verifyWebhook(req.body, req.header('GuidingHand-Signature'), process.env.GUIDINGHAND_WEBHOOK_SECRET);
|
|
97
118
|
if (event.type === 'task.completed') console.log(event.data.task.result);
|
|
119
|
+
if (event.type === 'task.question_answered') console.log(event.data.answer.answered_by, event.data.answer.answer);
|
|
98
120
|
res.sendStatus(200);
|
|
99
121
|
});
|
|
100
122
|
```
|
|
@@ -110,11 +132,11 @@ Every error is a `GuidingHandError` with `status`, `type`, `message` and `extra`
|
|
|
110
132
|
| `PaymentRequiredError` | 402 | the org's plan doesn't allow it (free minutes used up) |
|
|
111
133
|
| `PermissionDeniedError` | 403 | your role can't do this |
|
|
112
134
|
| `NotFoundError` | 404 | not in this org |
|
|
113
|
-
| `ConflictError` | 409 | no computer connected, a task already running (`extra.active_task_id`), nothing pending |
|
|
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`) |
|
|
114
136
|
| `RateLimitError` | 429 | too many requests, or the plan's tasks at once |
|
|
115
137
|
| `APIError` | 5xx | our side |
|
|
116
138
|
| `APIConnectionError`, `TimeoutError` | | network |
|
|
117
|
-
| `NeedsInputError` | | `run()` got a question or approval without a handler (`error.task`) |
|
|
139
|
+
| `NeedsInputError` | | `run()` got a question (that the customer can't answer) or an approval without a handler (`error.task`) |
|
|
118
140
|
| `SessionExpiredError` | | `waitForConnection()` on a code that expired |
|
|
119
141
|
|
|
120
142
|
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.2.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,12 @@ 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). Approvals always stay with the team. */
|
|
16
|
+
customer_answers: boolean;
|
|
11
17
|
is_default: boolean;
|
|
12
18
|
invite_url_template: string;
|
|
13
19
|
created_at: string | null;
|
|
@@ -38,11 +44,13 @@ export type Session = {
|
|
|
38
44
|
latest_task_id?: string | null;
|
|
39
45
|
};
|
|
40
46
|
export type TaskStatus = 'queued' | 'running' | 'waiting_for_user' | 'waiting_for_approval' | 'completed' | 'failed' | 'stopped';
|
|
47
|
+
/** customer_can_answer: the person at the computer can answer it on their screen too (the first answer is used). */
|
|
41
48
|
export type Question = {
|
|
42
49
|
type: 'question';
|
|
43
50
|
question_id: string;
|
|
44
51
|
question: string;
|
|
45
52
|
options: string[];
|
|
53
|
+
customer_can_answer: boolean;
|
|
46
54
|
};
|
|
47
55
|
export type Approval = {
|
|
48
56
|
type: 'approval';
|
|
@@ -51,6 +59,9 @@ export type Approval = {
|
|
|
51
59
|
risk: 'low' | 'medium' | 'high';
|
|
52
60
|
};
|
|
53
61
|
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). */
|
|
63
|
+
export type AnsweredBy = 'customer' | 'operator';
|
|
64
|
+
/** data: e.g. { action } for 'action', { question_id, question, options } for 'question', { question_id, answered_by } for 'answer' (whose message is the answer). */
|
|
54
65
|
export type TaskEvent = {
|
|
55
66
|
cursor: number;
|
|
56
67
|
type: EventType;
|
|
@@ -96,7 +107,7 @@ export type Recording = {
|
|
|
96
107
|
task_id: string;
|
|
97
108
|
frames: Frame[];
|
|
98
109
|
};
|
|
99
|
-
export type WebhookEventType = 'session.connected' | 'session.disconnected' | 'task.started' | 'task.waiting_for_user' | 'task.waiting_for_approval' | 'task.completed' | 'task.failed' | 'task.stopped';
|
|
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';
|
|
100
111
|
export type Webhook = {
|
|
101
112
|
object: 'webhook';
|
|
102
113
|
url: string | null;
|
|
@@ -105,6 +116,12 @@ export type Webhook = {
|
|
|
105
116
|
secret?: string;
|
|
106
117
|
event_types: WebhookEventType[];
|
|
107
118
|
};
|
|
119
|
+
export type QuestionAnswer = {
|
|
120
|
+
question_id: string;
|
|
121
|
+
answer: string;
|
|
122
|
+
answered_by: AnsweredBy;
|
|
123
|
+
};
|
|
124
|
+
/** data.answer: for task.question_answered. */
|
|
108
125
|
export type WebhookEvent = {
|
|
109
126
|
id: string;
|
|
110
127
|
type: WebhookEventType;
|
|
@@ -113,6 +130,7 @@ export type WebhookEvent = {
|
|
|
113
130
|
data: {
|
|
114
131
|
task?: Task;
|
|
115
132
|
session?: Session;
|
|
133
|
+
answer?: QuestionAnswer;
|
|
116
134
|
};
|
|
117
135
|
};
|
|
118
136
|
export type Page<T> = {
|
|
@@ -141,6 +159,7 @@ export declare class PermissionDeniedError extends GuidingHandError {
|
|
|
141
159
|
}
|
|
142
160
|
export declare class NotFoundError extends GuidingHandError {
|
|
143
161
|
}
|
|
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'. */
|
|
144
163
|
export declare class ConflictError extends GuidingHandError {
|
|
145
164
|
}
|
|
146
165
|
export declare class RateLimitError extends GuidingHandError {
|
|
@@ -153,7 +172,7 @@ export declare class TimeoutError extends GuidingHandError {
|
|
|
153
172
|
}
|
|
154
173
|
export declare class SessionExpiredError extends GuidingHandError {
|
|
155
174
|
}
|
|
156
|
-
/** A task waits on a question or approval and `run()` got no handler for it. */
|
|
175
|
+
/** A task waits on a question or approval and `run()` got no handler for it (a question the person at the computer can answer is left to them instead). */
|
|
157
176
|
export declare class NeedsInputError extends GuidingHandError {
|
|
158
177
|
task: Task;
|
|
159
178
|
constructor(task: Task);
|
|
@@ -205,6 +224,9 @@ export type AgentCreate = {
|
|
|
205
224
|
instructions?: string;
|
|
206
225
|
effort?: Effort;
|
|
207
226
|
greeting?: string;
|
|
227
|
+
display_name?: string;
|
|
228
|
+
narration?: boolean;
|
|
229
|
+
customer_answers?: boolean;
|
|
208
230
|
};
|
|
209
231
|
export type AgentUpdate = Partial<Omit<AgentCreate, 'agent_id'>>;
|
|
210
232
|
declare class Agents extends Resource {
|
|
@@ -264,8 +286,12 @@ export type Respond = {
|
|
|
264
286
|
export type RunOptions = TaskCreate & {
|
|
265
287
|
/** Every event, as it happens. */
|
|
266
288
|
onEvent?: (event: TaskEvent, task: Task) => void | Promise<void>;
|
|
267
|
-
/**
|
|
268
|
-
|
|
289
|
+
/**
|
|
290
|
+
* The agent asks something: return the answer. When the person at the computer can answer it on their screen
|
|
291
|
+
* (question.customer_can_answer), return null to leave it to them; without onQuestion, run() does that for
|
|
292
|
+
* every such question and throws NeedsInputError only for the others. If they answer first, yours is dropped.
|
|
293
|
+
*/
|
|
294
|
+
onQuestion?: (question: Question, task: Task) => string | null | undefined | Promise<string | null | undefined>;
|
|
269
295
|
/** The agent wants to do something consequential: return true (or 'approve') to let it, false (or 'deny', or { decision, note }) not to. */
|
|
270
296
|
onApproval?: (approval: Approval, task: Task) => boolean | 'approve' | 'deny' | {
|
|
271
297
|
decision: 'approve' | 'deny';
|
|
@@ -305,7 +331,7 @@ declare class Tasks extends Resource {
|
|
|
305
331
|
stream(taskId: string, { after }?: {
|
|
306
332
|
after?: number;
|
|
307
333
|
}): AsyncGenerator<TaskEvent, Task>;
|
|
308
|
-
/** Starts a task and sees it through: events to onEvent, questions to onQuestion, approvals to onApproval. Resolves with the finished task. */
|
|
334
|
+
/** Starts a task and sees it through: events to onEvent, questions to onQuestion (or the person at the computer), approvals to onApproval. Resolves with the finished task. */
|
|
309
335
|
run(sessionId: string, { onEvent, onQuestion, onApproval, timeout, ...params }: RunOptions): Promise<Task>;
|
|
310
336
|
}
|
|
311
337
|
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, questions the person at the computer can answer on their screen are 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.2.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 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 (a question the person at the computer can answer 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, approvals to onApproval. Resolves with the finished task. */
|
|
217
|
+
/** Starts a task and sees it through: events to onEvent, questions to onQuestion (or the person at the computer), approvals to onApproval. 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,12 +236,18 @@ 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 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
|
if (!onApproval)
|
|
@@ -250,7 +258,8 @@ class Tasks extends Resource {
|
|
|
250
258
|
}
|
|
251
259
|
}
|
|
252
260
|
catch (e) {
|
|
253
|
-
// Answered
|
|
261
|
+
// Answered somewhere else first (the person at the computer, the console, another process: extra.code
|
|
262
|
+
// 'already_answered') or the task ended meanwhile: keep following.
|
|
254
263
|
if (!(e instanceof ConflictError))
|
|
255
264
|
throw e;
|
|
256
265
|
}
|
package/dist/esm/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const VERSION = "0.
|
|
1
|
+
export declare const VERSION = "0.2.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,12 @@ 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). Approvals always stay with the team. */
|
|
16
|
+
customer_answers: boolean;
|
|
11
17
|
is_default: boolean;
|
|
12
18
|
invite_url_template: string;
|
|
13
19
|
created_at: string | null;
|
|
@@ -38,11 +44,13 @@ export type Session = {
|
|
|
38
44
|
latest_task_id?: string | null;
|
|
39
45
|
};
|
|
40
46
|
export type TaskStatus = 'queued' | 'running' | 'waiting_for_user' | 'waiting_for_approval' | 'completed' | 'failed' | 'stopped';
|
|
47
|
+
/** customer_can_answer: the person at the computer can answer it on their screen too (the first answer is used). */
|
|
41
48
|
export type Question = {
|
|
42
49
|
type: 'question';
|
|
43
50
|
question_id: string;
|
|
44
51
|
question: string;
|
|
45
52
|
options: string[];
|
|
53
|
+
customer_can_answer: boolean;
|
|
46
54
|
};
|
|
47
55
|
export type Approval = {
|
|
48
56
|
type: 'approval';
|
|
@@ -51,6 +59,9 @@ export type Approval = {
|
|
|
51
59
|
risk: 'low' | 'medium' | 'high';
|
|
52
60
|
};
|
|
53
61
|
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). */
|
|
63
|
+
export type AnsweredBy = 'customer' | 'operator';
|
|
64
|
+
/** data: e.g. { action } for 'action', { question_id, question, options } for 'question', { question_id, answered_by } for 'answer' (whose message is the answer). */
|
|
54
65
|
export type TaskEvent = {
|
|
55
66
|
cursor: number;
|
|
56
67
|
type: EventType;
|
|
@@ -96,7 +107,7 @@ export type Recording = {
|
|
|
96
107
|
task_id: string;
|
|
97
108
|
frames: Frame[];
|
|
98
109
|
};
|
|
99
|
-
export type WebhookEventType = 'session.connected' | 'session.disconnected' | 'task.started' | 'task.waiting_for_user' | 'task.waiting_for_approval' | 'task.completed' | 'task.failed' | 'task.stopped';
|
|
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';
|
|
100
111
|
export type Webhook = {
|
|
101
112
|
object: 'webhook';
|
|
102
113
|
url: string | null;
|
|
@@ -105,6 +116,12 @@ export type Webhook = {
|
|
|
105
116
|
secret?: string;
|
|
106
117
|
event_types: WebhookEventType[];
|
|
107
118
|
};
|
|
119
|
+
export type QuestionAnswer = {
|
|
120
|
+
question_id: string;
|
|
121
|
+
answer: string;
|
|
122
|
+
answered_by: AnsweredBy;
|
|
123
|
+
};
|
|
124
|
+
/** data.answer: for task.question_answered. */
|
|
108
125
|
export type WebhookEvent = {
|
|
109
126
|
id: string;
|
|
110
127
|
type: WebhookEventType;
|
|
@@ -113,6 +130,7 @@ export type WebhookEvent = {
|
|
|
113
130
|
data: {
|
|
114
131
|
task?: Task;
|
|
115
132
|
session?: Session;
|
|
133
|
+
answer?: QuestionAnswer;
|
|
116
134
|
};
|
|
117
135
|
};
|
|
118
136
|
export type Page<T> = {
|
|
@@ -141,6 +159,7 @@ export declare class PermissionDeniedError extends GuidingHandError {
|
|
|
141
159
|
}
|
|
142
160
|
export declare class NotFoundError extends GuidingHandError {
|
|
143
161
|
}
|
|
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'. */
|
|
144
163
|
export declare class ConflictError extends GuidingHandError {
|
|
145
164
|
}
|
|
146
165
|
export declare class RateLimitError extends GuidingHandError {
|
|
@@ -153,7 +172,7 @@ export declare class TimeoutError extends GuidingHandError {
|
|
|
153
172
|
}
|
|
154
173
|
export declare class SessionExpiredError extends GuidingHandError {
|
|
155
174
|
}
|
|
156
|
-
/** A task waits on a question or approval and `run()` got no handler for it. */
|
|
175
|
+
/** A task waits on a question or approval and `run()` got no handler for it (a question the person at the computer can answer is left to them instead). */
|
|
157
176
|
export declare class NeedsInputError extends GuidingHandError {
|
|
158
177
|
task: Task;
|
|
159
178
|
constructor(task: Task);
|
|
@@ -205,6 +224,9 @@ export type AgentCreate = {
|
|
|
205
224
|
instructions?: string;
|
|
206
225
|
effort?: Effort;
|
|
207
226
|
greeting?: string;
|
|
227
|
+
display_name?: string;
|
|
228
|
+
narration?: boolean;
|
|
229
|
+
customer_answers?: boolean;
|
|
208
230
|
};
|
|
209
231
|
export type AgentUpdate = Partial<Omit<AgentCreate, 'agent_id'>>;
|
|
210
232
|
declare class Agents extends Resource {
|
|
@@ -264,8 +286,12 @@ export type Respond = {
|
|
|
264
286
|
export type RunOptions = TaskCreate & {
|
|
265
287
|
/** Every event, as it happens. */
|
|
266
288
|
onEvent?: (event: TaskEvent, task: Task) => void | Promise<void>;
|
|
267
|
-
/**
|
|
268
|
-
|
|
289
|
+
/**
|
|
290
|
+
* The agent asks something: return the answer. When the person at the computer can answer it on their screen
|
|
291
|
+
* (question.customer_can_answer), return null to leave it to them; without onQuestion, run() does that for
|
|
292
|
+
* every such question and throws NeedsInputError only for the others. If they answer first, yours is dropped.
|
|
293
|
+
*/
|
|
294
|
+
onQuestion?: (question: Question, task: Task) => string | null | undefined | Promise<string | null | undefined>;
|
|
269
295
|
/** The agent wants to do something consequential: return true (or 'approve') to let it, false (or 'deny', or { decision, note }) not to. */
|
|
270
296
|
onApproval?: (approval: Approval, task: Task) => boolean | 'approve' | 'deny' | {
|
|
271
297
|
decision: 'approve' | 'deny';
|
|
@@ -305,7 +331,7 @@ declare class Tasks extends Resource {
|
|
|
305
331
|
stream(taskId: string, { after }?: {
|
|
306
332
|
after?: number;
|
|
307
333
|
}): AsyncGenerator<TaskEvent, Task>;
|
|
308
|
-
/** Starts a task and sees it through: events to onEvent, questions to onQuestion, approvals to onApproval. Resolves with the finished task. */
|
|
334
|
+
/** Starts a task and sees it through: events to onEvent, questions to onQuestion (or the person at the computer), approvals to onApproval. Resolves with the finished task. */
|
|
309
335
|
run(sessionId: string, { onEvent, onQuestion, onApproval, timeout, ...params }: RunOptions): Promise<Task>;
|
|
310
336
|
}
|
|
311
337
|
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, questions the person at the computer can answer on their screen are left to them)
|
|
11
|
+
export const VERSION = '0.2.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 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 (a question the person at the computer can answer 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, approvals to onApproval. Resolves with the finished task. */
|
|
198
|
+
/** Starts a task and sees it through: events to onEvent, questions to onQuestion (or the person at the computer), approvals to onApproval. 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,12 +217,18 @@ 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 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
|
if (!onApproval)
|
|
@@ -231,7 +239,8 @@ class Tasks extends Resource {
|
|
|
231
239
|
}
|
|
232
240
|
}
|
|
233
241
|
catch (e) {
|
|
234
|
-
// Answered
|
|
242
|
+
// Answered somewhere else first (the person at the computer, the console, another process: extra.code
|
|
243
|
+
// 'already_answered') or the task ended meanwhile: keep following.
|
|
235
244
|
if (!(e instanceof ConflictError))
|
|
236
245
|
throw e;
|
|
237
246
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "guidinghand",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.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",
|