@butlerbot/sdk 0.0.33 → 0.0.35
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/dist/config.d.ts +29 -0
- package/dist/config.js +34 -0
- package/dist/index.d.ts +38 -2
- package/dist/index.js +65 -4
- package/dist/link/link.d.ts +1 -0
- package/dist/link/link.js +1 -1
- package/dist/link/protocol.d.ts +7 -0
- package/dist/modules/api_request.d.ts +18 -0
- package/dist/modules/api_request.js +44 -0
- package/dist/modules/jobs.d.ts +48 -0
- package/dist/modules/jobs.js +72 -0
- package/dist/modules/outreach.d.ts +31 -0
- package/dist/modules/outreach.js +39 -0
- package/dist/modules/transport_link.d.ts +29 -0
- package/dist/modules/transport_link.js +230 -74
- package/dist/types/jobs/index.d.ts +1 -0
- package/dist/types/jobs/index.js +17 -0
- package/dist/types/jobs/job.d.ts +179 -0
- package/dist/types/jobs/job.js +30 -0
- package/dist/types/outreach/delivery.d.ts +78 -0
- package/dist/types/outreach/delivery.js +21 -0
- package/dist/types/outreach/index.d.ts +1 -0
- package/dist/types/outreach/index.js +17 -0
- package/dist/types/type_registry.d.ts +2 -0
- package/dist/types/type_registry.js +2 -0
- package/dist/util/api_error.d.ts +34 -0
- package/dist/util/api_error.js +42 -0
- package/package.json +1 -1
- package/readme.md +80 -4
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Outreach: what Alfred has told or asked this user outside a chat, and their answers.
|
|
3
|
+
*
|
|
4
|
+
* A delivery is the record itself, so the inbox listing is the delivery rather than a
|
|
5
|
+
* notification about one. Answering closes it, and when it belongs to a job the answer is put
|
|
6
|
+
* on that job's inbox.
|
|
7
|
+
*/
|
|
8
|
+
export declare const DELIVERY_INTENTS: readonly ["question", "update", "approval", "statement"];
|
|
9
|
+
export type DeliveryIntent = typeof DELIVERY_INTENTS[number];
|
|
10
|
+
/** The intents that leave something outstanding until the user answers. */
|
|
11
|
+
export declare const ANSWERABLE_DELIVERY_INTENTS: readonly DeliveryIntent[];
|
|
12
|
+
export declare const DELIVERY_SURFACES: readonly ["web", "discord"];
|
|
13
|
+
/** Where a delivery was put. */
|
|
14
|
+
export type DeliverySurfaceName = typeof DELIVERY_SURFACES[number];
|
|
15
|
+
export declare const DELIVERY_SURFACE_STATUSES: readonly ["pending", "sent", "failed"];
|
|
16
|
+
export type DeliverySurfaceStatus = typeof DELIVERY_SURFACE_STATUSES[number];
|
|
17
|
+
/** How one surface's send went. */
|
|
18
|
+
export type DeliverySurface = {
|
|
19
|
+
surface: DeliverySurfaceName;
|
|
20
|
+
status: DeliverySurfaceStatus;
|
|
21
|
+
/** Where on the surface it went: a Discord channel id, or "DM". */
|
|
22
|
+
channelId?: string | null;
|
|
23
|
+
/** What the channel called the message it sent. */
|
|
24
|
+
messageId?: string | null;
|
|
25
|
+
/** Why the last attempt failed. */
|
|
26
|
+
error?: string | null;
|
|
27
|
+
attempts: number;
|
|
28
|
+
/** When the last attempt was made, in UTC milliseconds. */
|
|
29
|
+
lastAt: number;
|
|
30
|
+
};
|
|
31
|
+
export declare const DELIVERY_ANSWER_VIA: readonly ["web", "discord", "chat", "tool"];
|
|
32
|
+
/** Which surface the answer came back on. */
|
|
33
|
+
export type DeliveryAnsweredVia = typeof DELIVERY_ANSWER_VIA[number];
|
|
34
|
+
export type DeliveryAnswer = {
|
|
35
|
+
at: number;
|
|
36
|
+
text: string;
|
|
37
|
+
via: DeliveryAnsweredVia;
|
|
38
|
+
/** On an approval, what the user decided. */
|
|
39
|
+
decision?: "approve" | "deny";
|
|
40
|
+
};
|
|
41
|
+
/** One delivery, as the inbox reads it. */
|
|
42
|
+
export type Delivery = {
|
|
43
|
+
deliveryId: string;
|
|
44
|
+
ownerUserId: string;
|
|
45
|
+
/** The chat this is about, so answering opens the conversation it belongs to. */
|
|
46
|
+
originConversationId: string | null;
|
|
47
|
+
/** The job this is about, when it is about one. */
|
|
48
|
+
jobId: string | null;
|
|
49
|
+
intent: DeliveryIntent;
|
|
50
|
+
message: string;
|
|
51
|
+
surfaces: DeliverySurface[];
|
|
52
|
+
/** Null until somebody answers. Only ever set once. */
|
|
53
|
+
answered: DeliveryAnswer | null;
|
|
54
|
+
created: number;
|
|
55
|
+
};
|
|
56
|
+
/** Whether a delivery is still waiting on the user. */
|
|
57
|
+
export declare function isDeliveryOpen(delivery: Delivery): boolean;
|
|
58
|
+
export type DeliveryListResponse = {
|
|
59
|
+
success: true;
|
|
60
|
+
deliveries: Delivery[];
|
|
61
|
+
page: number;
|
|
62
|
+
limit: number;
|
|
63
|
+
total: number;
|
|
64
|
+
};
|
|
65
|
+
/** What became of the answer once it was handed to the job that asked. */
|
|
66
|
+
export type DeliveryAnswerJobResult = {
|
|
67
|
+
jobId: string;
|
|
68
|
+
/** What the job did with it, or "dropped" when it could not take it. */
|
|
69
|
+
delivered: string;
|
|
70
|
+
/** Why it could not be delivered, when it could not. */
|
|
71
|
+
error?: string;
|
|
72
|
+
};
|
|
73
|
+
export type DeliveryAnswerResponse = {
|
|
74
|
+
success: true;
|
|
75
|
+
delivery: Delivery;
|
|
76
|
+
/** Present only when the delivery belonged to a job. */
|
|
77
|
+
job?: DeliveryAnswerJobResult;
|
|
78
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Outreach: what Alfred has told or asked this user outside a chat, and their answers.
|
|
4
|
+
*
|
|
5
|
+
* A delivery is the record itself, so the inbox listing is the delivery rather than a
|
|
6
|
+
* notification about one. Answering closes it, and when it belongs to a job the answer is put
|
|
7
|
+
* on that job's inbox.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.DELIVERY_ANSWER_VIA = exports.DELIVERY_SURFACE_STATUSES = exports.DELIVERY_SURFACES = exports.ANSWERABLE_DELIVERY_INTENTS = exports.DELIVERY_INTENTS = void 0;
|
|
11
|
+
exports.isDeliveryOpen = isDeliveryOpen;
|
|
12
|
+
exports.DELIVERY_INTENTS = ["question", "update", "approval", "statement"];
|
|
13
|
+
/** The intents that leave something outstanding until the user answers. */
|
|
14
|
+
exports.ANSWERABLE_DELIVERY_INTENTS = ["question", "approval"];
|
|
15
|
+
exports.DELIVERY_SURFACES = ["web", "discord"];
|
|
16
|
+
exports.DELIVERY_SURFACE_STATUSES = ["pending", "sent", "failed"];
|
|
17
|
+
exports.DELIVERY_ANSWER_VIA = ["web", "discord", "chat", "tool"];
|
|
18
|
+
/** Whether a delivery is still waiting on the user. */
|
|
19
|
+
function isDeliveryOpen(delivery) {
|
|
20
|
+
return exports.ANSWERABLE_DELIVERY_INTENTS.includes(delivery.intent) && !delivery.answered;
|
|
21
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./delivery";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./delivery"), exports);
|
|
@@ -19,4 +19,6 @@ __exportStar(require("./response/v4"), exports);
|
|
|
19
19
|
__exportStar(require("./response/v5"), exports);
|
|
20
20
|
__exportStar(require("./state/convo_state_response"), exports);
|
|
21
21
|
__exportStar(require("./conversation/v4/conversation_v4"), exports);
|
|
22
|
+
__exportStar(require("./jobs"), exports);
|
|
23
|
+
__exportStar(require("./outreach"), exports);
|
|
22
24
|
__exportStar(require("./error"), exports);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What an API call throws when the server did not answer with a success.
|
|
3
|
+
*
|
|
4
|
+
* One error class for every JSON route rather than a result type per method: the status is
|
|
5
|
+
* what a caller branches on, and it is carried here alongside whatever the server said, so
|
|
6
|
+
* "the job was already finished" (409) and "no job of yours has that id" (404) are told apart
|
|
7
|
+
* without parsing a message. `body` is the parsed response when there was one, so a 409 on a
|
|
8
|
+
* cancel still hands back the job, and a 409 on an answer still hands back the delivery.
|
|
9
|
+
*/
|
|
10
|
+
export declare class ButlerBotAPIError extends Error {
|
|
11
|
+
/** The HTTP status the server answered with, or 0 when the call never got a response. */
|
|
12
|
+
readonly status: number;
|
|
13
|
+
readonly statusText: string;
|
|
14
|
+
/** The server's short `error`, e.g. "Already finished". */
|
|
15
|
+
readonly error?: string;
|
|
16
|
+
/** The server's `errormessage`: the same thing in a sentence. */
|
|
17
|
+
readonly errormessage?: string;
|
|
18
|
+
/** The parsed response body, or the raw text when it was not JSON. */
|
|
19
|
+
readonly body: unknown;
|
|
20
|
+
constructor(options: {
|
|
21
|
+
action: string;
|
|
22
|
+
status: number;
|
|
23
|
+
statusText?: string;
|
|
24
|
+
error?: string;
|
|
25
|
+
errormessage?: string;
|
|
26
|
+
body?: unknown;
|
|
27
|
+
});
|
|
28
|
+
/** The resource is not there, or is not this key's. The two are answered alike on purpose. */
|
|
29
|
+
get isNotFound(): boolean;
|
|
30
|
+
/** Somebody got there first: the job had finished, or the delivery was already answered. */
|
|
31
|
+
get isConflict(): boolean;
|
|
32
|
+
/** The request itself was refused: a value the server would not take. */
|
|
33
|
+
get isBadRequest(): boolean;
|
|
34
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ButlerBotAPIError = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* What an API call throws when the server did not answer with a success.
|
|
6
|
+
*
|
|
7
|
+
* One error class for every JSON route rather than a result type per method: the status is
|
|
8
|
+
* what a caller branches on, and it is carried here alongside whatever the server said, so
|
|
9
|
+
* "the job was already finished" (409) and "no job of yours has that id" (404) are told apart
|
|
10
|
+
* without parsing a message. `body` is the parsed response when there was one, so a 409 on a
|
|
11
|
+
* cancel still hands back the job, and a 409 on an answer still hands back the delivery.
|
|
12
|
+
*/
|
|
13
|
+
class ButlerBotAPIError extends Error {
|
|
14
|
+
constructor(options) {
|
|
15
|
+
const detail = options.errormessage
|
|
16
|
+
|| options.error
|
|
17
|
+
// A body that was not JSON at all still says more than "Unknown error" does.
|
|
18
|
+
|| (typeof options.body === "string" && options.body ? options.body.slice(0, 500) : "Unknown error");
|
|
19
|
+
super(`Failed to ${options.action}: ${options.status} ${options.statusText || ""} - ${detail}`.replace(/\s+-/, " -"));
|
|
20
|
+
this.name = "ButlerBotAPIError";
|
|
21
|
+
this.status = options.status;
|
|
22
|
+
this.statusText = options.statusText || "";
|
|
23
|
+
this.error = options.error;
|
|
24
|
+
this.errormessage = options.errormessage;
|
|
25
|
+
this.body = options.body;
|
|
26
|
+
// `target: es2020` down-levels the subclass, which breaks `instanceof` without this.
|
|
27
|
+
Object.setPrototypeOf(this, ButlerBotAPIError.prototype);
|
|
28
|
+
}
|
|
29
|
+
/** The resource is not there, or is not this key's. The two are answered alike on purpose. */
|
|
30
|
+
get isNotFound() {
|
|
31
|
+
return this.status === 404;
|
|
32
|
+
}
|
|
33
|
+
/** Somebody got there first: the job had finished, or the delivery was already answered. */
|
|
34
|
+
get isConflict() {
|
|
35
|
+
return this.status === 409;
|
|
36
|
+
}
|
|
37
|
+
/** The request itself was refused: a value the server would not take. */
|
|
38
|
+
get isBadRequest() {
|
|
39
|
+
return this.status === 400;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
exports.ButlerBotAPIError = ButlerBotAPIError;
|
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -78,7 +78,12 @@ between you and the socket.
|
|
|
78
78
|
|
|
79
79
|
## Link
|
|
80
80
|
|
|
81
|
-
A Link is a live connection to Alfred
|
|
81
|
+
A Link is a live connection to Alfred, served by its own endpoint — `link.butler.now`,
|
|
82
|
+
not the core API server. `createLink` goes there by default; pass `linkUrl` to the client
|
|
83
|
+
(or `serverUrl` to `createLink`) to point somewhere else. A client given a `serverUrl` of
|
|
84
|
+
its own — a self-hosted stack — uses that for links too.
|
|
85
|
+
|
|
86
|
+
It does three things:
|
|
82
87
|
|
|
83
88
|
- **Tools** — Alfred calls code that runs on your machine
|
|
84
89
|
- **Hooks** — your code wakes the user's background agents when something happens
|
|
@@ -286,9 +291,15 @@ Which to use:
|
|
|
286
291
|
Best when you are already running a link for tools or hooks, or holding many
|
|
287
292
|
conversations at once — one socket carries them all.
|
|
288
293
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
294
|
+
A turn survives losing the connection it was asked for on. Sessions are ephemeral — the
|
|
295
|
+
server drops one with its socket — but the turn belongs to the conversation, so when the
|
|
296
|
+
socket goes mid-answer the SDK reconnects, rejoins the turn from the last event it gave
|
|
297
|
+
you, and carries on into the same stream. The same holds when the platform deploys
|
|
298
|
+
mid-turn and the answer is handed to another instance. A message that had not been
|
|
299
|
+
delivered yet is simply sent again on a fresh session; one that had is never sent twice.
|
|
300
|
+
|
|
301
|
+
If the turn cannot be picked back up within a minute, the stream ends with a failure that
|
|
302
|
+
says so — the reply may still have finished, and reopening the conversation will show it.
|
|
292
303
|
|
|
293
304
|
### Rejoining a turn already in progress
|
|
294
305
|
|
|
@@ -311,6 +322,71 @@ seen. A conversation with nothing running simply ends the stream.
|
|
|
311
322
|
Neither transport can cancel a turn: `close()` stops delivery locally, and the reply is
|
|
312
323
|
still generated and stored.
|
|
313
324
|
|
|
325
|
+
## Jobs
|
|
326
|
+
|
|
327
|
+
A job is long-running work Alfred does on its own, run as a sequence of short shifts against
|
|
328
|
+
a plan it writes. The client reads them and steers them; it does not run them.
|
|
329
|
+
|
|
330
|
+
```typescript
|
|
331
|
+
const { jobs, allowance, spentTodayUsd } = await client.listJobs({ status: "running", page: 1, limit: 20 });
|
|
332
|
+
const { job, plan, journal, openDeliveries, autonomyLine } = await client.getJob({ jobId });
|
|
333
|
+
|
|
334
|
+
await client.updateJob({ jobId, autonomy: "free", autonomyUntil: Date.now() + 86_400_000 });
|
|
335
|
+
await client.cancelJob({ jobId });
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
Paging is 1-based, and every listing answers with `page`, `limit` and `total`. A listing also
|
|
339
|
+
carries `allowance` — what this user's jobs may spend today and what is left of it, or
|
|
340
|
+
`{ perDayUsd: 0, source: null, refused, message }` when their plan does not include jobs.
|
|
341
|
+
|
|
342
|
+
`autonomy` is how far a job may act outward without asking: `ask`, `free` or `auto`.
|
|
343
|
+
`autonomyUntil` is when a loosened setting lapses back to asking, and `alwaysAsk` is what the
|
|
344
|
+
job asks about however free it otherwise is. `effectiveAutonomy` on a job is what the gate
|
|
345
|
+
actually reads, with the owner's default and any lapse already applied. What new jobs start
|
|
346
|
+
with, and what they may spend, is the user's own setting:
|
|
347
|
+
|
|
348
|
+
```typescript
|
|
349
|
+
await client.updateJobSettings({ allowanceUsd: 5, autonomy: "ask", alwaysAsk: ["spending money"] });
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
## Outreach
|
|
353
|
+
|
|
354
|
+
Outreach is what Alfred has told or asked this user outside a chat. The record is the inbox —
|
|
355
|
+
there is no separate notification — so a question a job is parked on is a delivery with
|
|
356
|
+
`intent: "question"` and no `answered`:
|
|
357
|
+
|
|
358
|
+
```typescript
|
|
359
|
+
const { deliveries } = await client.listDeliveries({ page: 1, limit: 20 });
|
|
360
|
+
const { delivery, job } = await client.answerDelivery({ deliveryId, text: "The one in Gardens" });
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
Answering closes the delivery, and when it belongs to a job the answer is put on that job's
|
|
364
|
+
inbox, which wakes a job that was parked waiting for it — `job.delivered` says what became of
|
|
365
|
+
it. An approval takes `decision: "approve" | "deny"` alongside the text.
|
|
366
|
+
|
|
367
|
+
### When a call fails
|
|
368
|
+
|
|
369
|
+
Every jobs and outreach call throws `ButlerBotAPIError` when the server does not answer with a
|
|
370
|
+
success. The status is on the error, so the cases worth branching on are told apart without
|
|
371
|
+
reading a message, and the parsed body is kept — a rejected cancel still carries the job, a
|
|
372
|
+
rejected answer still carries the delivery:
|
|
373
|
+
|
|
374
|
+
```typescript
|
|
375
|
+
import { ButlerBotAPIError } from "@butlerbot/sdk";
|
|
376
|
+
|
|
377
|
+
try {
|
|
378
|
+
await client.cancelJob({ jobId });
|
|
379
|
+
} catch (error) {
|
|
380
|
+
if (error instanceof ButlerBotAPIError && error.isConflict) {
|
|
381
|
+
// 409: it had already finished. `error.body.job` is how it settled.
|
|
382
|
+
} else throw error;
|
|
383
|
+
}
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
`isNotFound` (404), `isConflict` (409) and `isBadRequest` (400) are the three; `status`,
|
|
387
|
+
`error` and `errormessage` are the server's own words. A job or delivery that is not this key's
|
|
388
|
+
is answered exactly as one that does not exist, so a 404 means either.
|
|
389
|
+
|
|
314
390
|
## Environment
|
|
315
391
|
|
|
316
392
|
Node 18+. Node 22 and every browser have a built-in WebSocket; on older Node, install
|