@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
|
@@ -3,6 +3,17 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.LinkConversationTransport = void 0;
|
|
4
4
|
const protocol_1 = require("../link/protocol");
|
|
5
5
|
const transport_1 = require("./transport");
|
|
6
|
+
/**
|
|
7
|
+
* How long to keep trying to pick a lost turn back up before giving up on it.
|
|
8
|
+
*
|
|
9
|
+
* A turn belongs to its conversation, not to the socket that asked for one, so neither losing
|
|
10
|
+
* the connection nor losing the instance answering it — which is what a deploy does — ends it.
|
|
11
|
+
* Long enough to outlast a deploy of either service, short enough that a caller awaiting a
|
|
12
|
+
* reply is not left there forever when the turn really is gone.
|
|
13
|
+
*/
|
|
14
|
+
const RESUME_WINDOW_MS = 60000;
|
|
15
|
+
/** Waits between attempts to pick a turn back up. The last value repeats. */
|
|
16
|
+
const RESUME_BACKOFF_MS = [250, 500, 1000, 2000, 4000, 5000];
|
|
6
17
|
/**
|
|
7
18
|
* Carries a turn over an existing Link connection.
|
|
8
19
|
*
|
|
@@ -18,20 +29,20 @@ class LinkConversationTransport {
|
|
|
18
29
|
link.on("disconnect", () => { this.sessionId = undefined; });
|
|
19
30
|
}
|
|
20
31
|
send(request, handlers) {
|
|
21
|
-
|
|
32
|
+
const listening = { closed: false };
|
|
22
33
|
const deliver = {
|
|
23
|
-
payload: (payload) => { if (!closed)
|
|
34
|
+
payload: (payload) => { if (!listening.closed)
|
|
24
35
|
handlers.payload(payload); },
|
|
25
|
-
convoId: (convoId) => { if (!closed)
|
|
36
|
+
convoId: (convoId) => { if (!listening.closed)
|
|
26
37
|
handlers.convoId(convoId); },
|
|
27
38
|
};
|
|
28
|
-
void this.runTurn(request, deliver, true).catch((error) => {
|
|
39
|
+
void this.runTurn(request, deliver, true, listening).catch((error) => {
|
|
29
40
|
const failure = error instanceof protocol_1.LinkError
|
|
30
41
|
? (0, transport_1.failurePayload)(error.code, error.message, error.message, request.chatId)
|
|
31
42
|
: (0, transport_1.failurePayload)("link_error", String(error), "I'm afraid the connection to Alfred failed.", request.chatId);
|
|
32
43
|
deliver.payload(failure);
|
|
33
44
|
});
|
|
34
|
-
return { close: () => { closed = true; } };
|
|
45
|
+
return { close: () => { listening.closed = true; } };
|
|
35
46
|
}
|
|
36
47
|
/**
|
|
37
48
|
* Stops a running turn.
|
|
@@ -96,57 +107,46 @@ class LinkConversationTransport {
|
|
|
96
107
|
* ordinary answer for one that is simply idle.
|
|
97
108
|
*/
|
|
98
109
|
attach(request, handlers) {
|
|
99
|
-
|
|
100
|
-
const deliver = (payload) => { if (!closed)
|
|
110
|
+
const listening = { closed: false };
|
|
111
|
+
const deliver = (payload) => { if (!listening.closed)
|
|
101
112
|
handlers.payload(payload); };
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
timeoutMs: 0,
|
|
108
|
-
isDone: (frame) => frame.type === "conversation.done",
|
|
109
|
-
onFrame: (frame) => {
|
|
110
|
-
if (frame.type === "conversation.event") {
|
|
111
|
-
const payload = frame.payload;
|
|
112
|
-
const event = payload.event;
|
|
113
|
-
const final = event.type === "response_status" && Boolean(event.payload?.completed);
|
|
114
|
-
deliver({
|
|
115
|
-
success: true,
|
|
116
|
-
data: {
|
|
117
|
-
response: event,
|
|
118
|
-
convoId: payload.chatId ?? request.chatId,
|
|
119
|
-
...(final ? { quitStream: true } : {}),
|
|
120
|
-
},
|
|
121
|
-
});
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
124
|
-
if (frame.type === "conversation.notice") {
|
|
125
|
-
deliver((0, transport_1.noticePayload)(frame.payload.message, frame.payload.chatId ?? request.chatId));
|
|
126
|
-
}
|
|
127
|
-
},
|
|
128
|
-
});
|
|
129
|
-
void watching.then((done) => {
|
|
130
|
-
const payload = done.payload;
|
|
113
|
+
const progress = { chatId: request.chatId, lastEventId: request.afterEventId, sawCompletion: false };
|
|
114
|
+
// Watching is allowed to end in silence, which is why `quietWhenGone` is true here and
|
|
115
|
+
// false for a turn of our own: nobody is waiting on an answer to a question they asked.
|
|
116
|
+
const resume = () => this.resume(request.chatId, progress, deliver, listening, { quietWhenGone: true });
|
|
117
|
+
void this.watch(request.chatId, progress, deliver).then(async (payload) => {
|
|
131
118
|
if (payload.ok)
|
|
132
119
|
return;
|
|
133
120
|
// Nothing running is not a failure: the caller asked to watch a conversation
|
|
134
121
|
// that has nothing to watch, and the stream simply ends.
|
|
135
122
|
if (payload.code === "no_active_turn")
|
|
136
123
|
return;
|
|
124
|
+
// The turn is alive, somewhere this connection can no longer see. Following it is
|
|
125
|
+
// the entire point of being here.
|
|
126
|
+
if (payload.code === "turn_suspended") {
|
|
127
|
+
if (payload.lastEventId)
|
|
128
|
+
progress.lastEventId = payload.lastEventId;
|
|
129
|
+
await resume();
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
137
132
|
deliver((0, transport_1.failurePayload)(payload.code ?? "link_error", payload.error ?? "The turn could not be watched.", payload.message ?? payload.error ?? "I'm afraid I couldn't follow that response.", payload.chatId ?? request.chatId));
|
|
138
|
-
}, (error) => {
|
|
133
|
+
}, async (error) => {
|
|
139
134
|
if (error instanceof protocol_1.LinkError && error.code === "no_active_turn")
|
|
140
135
|
return;
|
|
136
|
+
// The socket went while we were watching. The turn did not go with it.
|
|
137
|
+
if (error instanceof protocol_1.LinkError && error.code === "disconnected") {
|
|
138
|
+
await resume();
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
141
|
deliver(error instanceof protocol_1.LinkError
|
|
142
142
|
? (0, transport_1.failurePayload)(error.code, error.message, error.message, request.chatId)
|
|
143
143
|
: (0, transport_1.failurePayload)("link_error", String(error), "I'm afraid the connection to Alfred failed.", request.chatId));
|
|
144
144
|
});
|
|
145
145
|
return {
|
|
146
146
|
close: () => {
|
|
147
|
-
if (closed)
|
|
147
|
+
if (listening.closed)
|
|
148
148
|
return;
|
|
149
|
-
closed = true;
|
|
149
|
+
listening.closed = true;
|
|
150
150
|
// Best-effort: a socket that has gone has already ended the watch for us.
|
|
151
151
|
try {
|
|
152
152
|
this.link.send("conversation.detach", { chatId: request.chatId });
|
|
@@ -157,15 +157,14 @@ class LinkConversationTransport {
|
|
|
157
157
|
},
|
|
158
158
|
};
|
|
159
159
|
}
|
|
160
|
-
async runTurn(request, handlers, mayRetry) {
|
|
160
|
+
async runTurn(request, handlers, mayRetry, listening) {
|
|
161
161
|
const sessionId = await this.session(request);
|
|
162
|
-
|
|
162
|
+
const progress = { chatId: request.chatId ?? this.sessionChatId, sawCompletion: false };
|
|
163
163
|
let announcedChatId = false;
|
|
164
|
-
let sawCompletion = false;
|
|
165
164
|
const learnChatId = (candidate) => {
|
|
166
165
|
if (!candidate)
|
|
167
166
|
return;
|
|
168
|
-
chatId = candidate;
|
|
167
|
+
progress.chatId = candidate;
|
|
169
168
|
this.sessionChatId = candidate;
|
|
170
169
|
handlers.convoId(candidate);
|
|
171
170
|
// Mirrors the HTTP transport's first byte, which tells a client which
|
|
@@ -176,13 +175,99 @@ class LinkConversationTransport {
|
|
|
176
175
|
handlers.payload((0, transport_1.convoStartedPayload)(candidate));
|
|
177
176
|
}
|
|
178
177
|
};
|
|
179
|
-
learnChatId(chatId);
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
178
|
+
learnChatId(progress.chatId);
|
|
179
|
+
let done;
|
|
180
|
+
try {
|
|
181
|
+
done = await this.link.exchange("conversation.chat", {
|
|
182
|
+
sessionId,
|
|
183
|
+
message: request.message,
|
|
184
|
+
...(request.model ? { model: request.model } : {}),
|
|
185
|
+
...(request.instructions ? { instructions: request.instructions } : {}),
|
|
186
|
+
...(request.personality ? { personality: request.personality } : {}),
|
|
187
|
+
}, {
|
|
188
|
+
// A turn takes as long as it takes; only the transport dying ends it early.
|
|
189
|
+
timeoutMs: 0,
|
|
190
|
+
isDone: (frame) => frame.type === "conversation.done",
|
|
191
|
+
onFrame: (frame) => {
|
|
192
|
+
if (frame.type === "conversation.event") {
|
|
193
|
+
const payload = frame.payload;
|
|
194
|
+
learnChatId(payload.chatId);
|
|
195
|
+
if (payload.eventId)
|
|
196
|
+
progress.lastEventId = payload.eventId;
|
|
197
|
+
const event = payload.event;
|
|
198
|
+
const final = event.type === "response_status" && Boolean(event.payload?.completed);
|
|
199
|
+
if (final)
|
|
200
|
+
progress.sawCompletion = true;
|
|
201
|
+
handlers.payload({
|
|
202
|
+
success: true,
|
|
203
|
+
data: {
|
|
204
|
+
response: event,
|
|
205
|
+
...(payload.chatId ?? progress.chatId ? { convoId: payload.chatId ?? progress.chatId } : {}),
|
|
206
|
+
...(final ? { quitStream: true } : {}),
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (frame.type === "conversation.notice") {
|
|
212
|
+
const payload = frame.payload;
|
|
213
|
+
learnChatId(payload.chatId);
|
|
214
|
+
handlers.payload((0, transport_1.noticePayload)(payload.message, payload.chatId ?? progress.chatId));
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
catch (error) {
|
|
220
|
+
// The socket went while the turn was running. The turn did not go with it: it
|
|
221
|
+
// belongs to the conversation, and the conversation outlives this connection.
|
|
222
|
+
if (progress.chatId && error instanceof protocol_1.LinkError && error.code === "disconnected") {
|
|
223
|
+
await this.resume(progress.chatId, progress, handlers.payload, listening, { quietWhenGone: false });
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
throw error;
|
|
227
|
+
}
|
|
228
|
+
const payload = done.payload;
|
|
229
|
+
learnChatId(payload.chatId);
|
|
230
|
+
if (payload.ok) {
|
|
231
|
+
// Nearly always the pipeline's own completion event has already closed the
|
|
232
|
+
// stream; this is for the turn that ended without one.
|
|
233
|
+
if (!progress.sawCompletion)
|
|
234
|
+
handlers.payload((0, transport_1.completedPayload)(payload.chatId ?? progress.chatId));
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
// The session died with a connection we have since replaced. Reopening it is
|
|
238
|
+
// invisible to the caller, and the message has not been delivered yet.
|
|
239
|
+
if (payload.code === "unknown_session" && mayRetry) {
|
|
240
|
+
this.sessionId = undefined;
|
|
241
|
+
await this.runTurn(request, handlers, false, listening);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
// Core suspended the turn at a deploy and handed it to another instance, and the link
|
|
245
|
+
// service followed it as far as it could. The answer is still being written; rejoining
|
|
246
|
+
// it is the difference between a deploy costing a reply and costing nothing.
|
|
247
|
+
if (payload.code === "turn_suspended" && progress.chatId) {
|
|
248
|
+
if (payload.lastEventId)
|
|
249
|
+
progress.lastEventId = payload.lastEventId;
|
|
250
|
+
await this.resume(progress.chatId, progress, handlers.payload, listening, { quietWhenGone: false });
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
handlers.payload((0, transport_1.failurePayload)(
|
|
254
|
+
// `turn_failed` means the dialogue itself failed, and then `error` holds the
|
|
255
|
+
// code the HTTP transport would have reported.
|
|
256
|
+
payload.code === "turn_failed" ? payload.error ?? payload.code : payload.code ?? "link_error", payload.error ?? "The turn failed.", payload.message ?? payload.error ?? "I'm afraid that turn could not be completed.", payload.chatId ?? progress.chatId));
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* One `conversation.attach`: streams a turn's events to the caller and resolves with how
|
|
260
|
+
* that watch ended.
|
|
261
|
+
*
|
|
262
|
+
* Shared by watching somebody else's turn and by rejoining one of our own, because from
|
|
263
|
+
* here the two are the same act. `progress` is carried rather than returned: a watch that
|
|
264
|
+
* dies halfway still has to leave behind where it got to, or a resume would replay from
|
|
265
|
+
* the beginning and the caller would read the answer twice.
|
|
266
|
+
*/
|
|
267
|
+
async watch(chatId, progress, deliver) {
|
|
268
|
+
const done = await this.link.exchange("conversation.attach", {
|
|
269
|
+
chatId,
|
|
270
|
+
...(progress.lastEventId ? { afterEventId: progress.lastEventId } : {}),
|
|
186
271
|
}, {
|
|
187
272
|
// A turn takes as long as it takes; only the transport dying ends it early.
|
|
188
273
|
timeoutMs: 0,
|
|
@@ -190,48 +275,112 @@ class LinkConversationTransport {
|
|
|
190
275
|
onFrame: (frame) => {
|
|
191
276
|
if (frame.type === "conversation.event") {
|
|
192
277
|
const payload = frame.payload;
|
|
193
|
-
|
|
278
|
+
if (payload.eventId)
|
|
279
|
+
progress.lastEventId = payload.eventId;
|
|
194
280
|
const event = payload.event;
|
|
195
281
|
const final = event.type === "response_status" && Boolean(event.payload?.completed);
|
|
196
282
|
if (final)
|
|
197
|
-
sawCompletion = true;
|
|
198
|
-
|
|
283
|
+
progress.sawCompletion = true;
|
|
284
|
+
deliver({
|
|
199
285
|
success: true,
|
|
200
286
|
data: {
|
|
201
287
|
response: event,
|
|
202
|
-
|
|
288
|
+
convoId: payload.chatId ?? chatId,
|
|
203
289
|
...(final ? { quitStream: true } : {}),
|
|
204
290
|
},
|
|
205
291
|
});
|
|
206
292
|
return;
|
|
207
293
|
}
|
|
208
294
|
if (frame.type === "conversation.notice") {
|
|
209
|
-
|
|
210
|
-
learnChatId(payload.chatId);
|
|
211
|
-
handlers.payload((0, transport_1.noticePayload)(payload.message, payload.chatId ?? chatId));
|
|
295
|
+
deliver((0, transport_1.noticePayload)(frame.payload.message, frame.payload.chatId ?? chatId));
|
|
212
296
|
}
|
|
213
297
|
},
|
|
214
298
|
});
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
299
|
+
return done.payload;
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Picks a turn back up after losing sight of it.
|
|
303
|
+
*
|
|
304
|
+
* Two ways to lose one, one way to get it back. The socket can go — a deploy of the link
|
|
305
|
+
* service, a proxy timing out — which rejects the exchange carrying the turn. Or core can
|
|
306
|
+
* suspend the turn at its own deploy and hand it to another instance, which the link
|
|
307
|
+
* service follows for two minutes before giving up and saying `turn_suspended`. Either way
|
|
308
|
+
* the turn is still being answered and the conversation still holds it, so this re-attaches
|
|
309
|
+
* from the last event the caller was actually given and the stream reads as one answer.
|
|
310
|
+
*
|
|
311
|
+
* `no_active_turn` is the ambiguous reply and it is deliberately not treated as an ending:
|
|
312
|
+
* during a handover it means "not picked up yet" far more often than it means "gone", and
|
|
313
|
+
* the window is what decides between them.
|
|
314
|
+
*
|
|
315
|
+
* Nothing here throws. It runs behind a stream the caller already holds, so the outcomes
|
|
316
|
+
* that matter are the ones delivered into it: a completion, or a failure that says plainly
|
|
317
|
+
* that the answer was lost track of rather than that it failed.
|
|
318
|
+
*/
|
|
319
|
+
async resume(chatId, progress, deliver, listening, options) {
|
|
320
|
+
const deadline = Date.now() + RESUME_WINDOW_MS;
|
|
321
|
+
for (let attempt = 0; !listening.closed && Date.now() < deadline; attempt++) {
|
|
322
|
+
if (attempt > 0)
|
|
323
|
+
await pause(RESUME_BACKOFF_MS[Math.min(attempt - 1, RESUME_BACKOFF_MS.length - 1)]);
|
|
324
|
+
if (listening.closed)
|
|
325
|
+
return;
|
|
326
|
+
try {
|
|
327
|
+
// The link reconnects on its own; this waits for it rather than racing it.
|
|
328
|
+
await this.link.ready();
|
|
329
|
+
}
|
|
330
|
+
catch (error) {
|
|
331
|
+
// Closed for good means nobody is coming back. Anything else is the link still
|
|
332
|
+
// being down, which is exactly what the window is for.
|
|
333
|
+
if (error instanceof protocol_1.LinkError && error.code === "closed")
|
|
334
|
+
break;
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
// An attachment from before may still be registered against a connection that is
|
|
338
|
+
// itself still alive, and the server allows only one per conversation. Dropping it
|
|
339
|
+
// first costs nothing when there is none to drop.
|
|
340
|
+
try {
|
|
341
|
+
this.link.send("conversation.detach", { chatId });
|
|
342
|
+
}
|
|
343
|
+
catch {
|
|
344
|
+
// Not connected. The server dropped the attachment with the socket.
|
|
345
|
+
}
|
|
346
|
+
let payload;
|
|
347
|
+
try {
|
|
348
|
+
payload = await this.watch(chatId, progress, deliver);
|
|
349
|
+
}
|
|
350
|
+
catch (error) {
|
|
351
|
+
if (error instanceof protocol_1.LinkError && error.code === "closed")
|
|
352
|
+
break;
|
|
353
|
+
if (error instanceof protocol_1.LinkError && error.code === "no_active_turn" && options.quietWhenGone)
|
|
354
|
+
return;
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
if (payload.ok) {
|
|
358
|
+
if (!progress.sawCompletion)
|
|
359
|
+
deliver((0, transport_1.completedPayload)(payload.chatId ?? chatId));
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
// Not picked up yet, or suspended again mid-hop. Both mean it is still moving.
|
|
363
|
+
if (payload.code === "no_active_turn") {
|
|
364
|
+
if (options.quietWhenGone)
|
|
365
|
+
return;
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
if (payload.code === "turn_suspended") {
|
|
369
|
+
if (payload.lastEventId)
|
|
370
|
+
progress.lastEventId = payload.lastEventId;
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
deliver((0, transport_1.failurePayload)(payload.code ?? "link_error", payload.error ?? "The turn could not be picked back up.", payload.message ?? payload.error ?? "I'm afraid I couldn't follow that response.", payload.chatId ?? chatId));
|
|
222
374
|
return;
|
|
223
375
|
}
|
|
224
|
-
|
|
225
|
-
// invisible to the caller, and the message has not been delivered yet.
|
|
226
|
-
if (payload.code === "unknown_session" && mayRetry) {
|
|
227
|
-
this.sessionId = undefined;
|
|
228
|
-
await this.runTurn(request, handlers, false);
|
|
376
|
+
if (listening.closed)
|
|
229
377
|
return;
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
378
|
+
// The completion already reached the caller; there is nothing left to say.
|
|
379
|
+
if (progress.sawCompletion)
|
|
380
|
+
return;
|
|
381
|
+
if (options.quietWhenGone)
|
|
382
|
+
return;
|
|
383
|
+
deliver((0, transport_1.failurePayload)("turn_suspended", "The turn could not be picked back up.", "I'm afraid I lost track of that answer. It may well have finished — reopen the conversation to see where it got to.", chatId));
|
|
235
384
|
}
|
|
236
385
|
/** Opens a session, or reuses the open one when it is for the same conversation. */
|
|
237
386
|
async session(request) {
|
|
@@ -257,3 +406,10 @@ class LinkConversationTransport {
|
|
|
257
406
|
}
|
|
258
407
|
}
|
|
259
408
|
exports.LinkConversationTransport = LinkConversationTransport;
|
|
409
|
+
/** Waits, without keeping a Node process alive on its own. */
|
|
410
|
+
function pause(ms) {
|
|
411
|
+
return new Promise(resolve => {
|
|
412
|
+
const timer = setTimeout(resolve, ms);
|
|
413
|
+
timer.unref?.();
|
|
414
|
+
});
|
|
415
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./job";
|
|
@@ -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("./job"), exports);
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import type { Delivery } from "../outreach/delivery";
|
|
2
|
+
/**
|
|
3
|
+
* A job: long-running work Alfred does on its own, run as a sequence of short shifts.
|
|
4
|
+
*
|
|
5
|
+
* These are the shapes the jobs routes answer with. The server flattens a job row into a
|
|
6
|
+
* `JobView` before it leaves, and this is that view rather than the row.
|
|
7
|
+
*/
|
|
8
|
+
export declare const JOB_STATUSES: readonly ["queued", "running", "blocked", "waiting_user", "waiting_approval", "waiting_child", "waiting_budget", "review", "done", "failed", "cancelled"];
|
|
9
|
+
export type JobStatus = typeof JOB_STATUSES[number];
|
|
10
|
+
/** The statuses a job never leaves. */
|
|
11
|
+
export declare const TERMINAL_JOB_STATUSES: readonly JobStatus[];
|
|
12
|
+
export declare const JOB_AUTONOMY_MODES: readonly ["ask", "free", "auto"];
|
|
13
|
+
/** How far a job may act outward on its own: ask first, act freely, or act and report. */
|
|
14
|
+
export type JobAutonomy = typeof JOB_AUTONOMY_MODES[number];
|
|
15
|
+
/** One job as a listing or a detail read shows it. */
|
|
16
|
+
export type JobView = {
|
|
17
|
+
jobId: string;
|
|
18
|
+
/** A short name for listings, derived from the goal when nobody gave one. */
|
|
19
|
+
title: string;
|
|
20
|
+
/** The user's words, verbatim: what the job was asked to do. */
|
|
21
|
+
goal: string;
|
|
22
|
+
status: JobStatus;
|
|
23
|
+
/** Why it is where it is, specific enough to act on. */
|
|
24
|
+
statusDetail: string | null;
|
|
25
|
+
/** A one-line note the running shift may leave. */
|
|
26
|
+
progress: string | null;
|
|
27
|
+
/** The phase being worked, or the last one that was. */
|
|
28
|
+
currentPhaseId: string | null;
|
|
29
|
+
shiftsRun: number;
|
|
30
|
+
/** How many times the job has been reviewed. */
|
|
31
|
+
reviewRounds: number;
|
|
32
|
+
/** What the job has spent in total, in USD. */
|
|
33
|
+
spentUsd: number;
|
|
34
|
+
/** What the job has spent today, in USD. */
|
|
35
|
+
spendTodayUsd: number;
|
|
36
|
+
/** How many questions of this job's are waiting on the user. */
|
|
37
|
+
openQuestions: number;
|
|
38
|
+
lastRunAt: number;
|
|
39
|
+
/** The earliest the runner may pick it up again. */
|
|
40
|
+
wakeAt: number;
|
|
41
|
+
created: number;
|
|
42
|
+
/** `card://…/plan.md`, once a planning shift has written it. */
|
|
43
|
+
planCardUri: string | null;
|
|
44
|
+
/** `card://…/journal.md`, appended to as the job runs. */
|
|
45
|
+
journalCardUri: string | null;
|
|
46
|
+
/** The chat the job was asked for in, and where it reports back. */
|
|
47
|
+
originConversationId: string | null;
|
|
48
|
+
/** What was chosen for this job, or null when nothing was. */
|
|
49
|
+
autonomy: JobAutonomy | null;
|
|
50
|
+
/** When `autonomy` lapses back to asking, in UTC milliseconds. */
|
|
51
|
+
autonomyUntil: number | null;
|
|
52
|
+
/** Things this job asks about however free it otherwise is. */
|
|
53
|
+
alwaysAsk: string[];
|
|
54
|
+
/** What the gate actually reads, with the owner's setting and any lapse applied. */
|
|
55
|
+
effectiveAutonomy: JobAutonomy;
|
|
56
|
+
};
|
|
57
|
+
/** What the owner's jobs may spend today, and where that figure comes from. */
|
|
58
|
+
export type JobAllowanceGranted = {
|
|
59
|
+
perDayUsd: number;
|
|
60
|
+
/** What set the figure: the owner's own allowance, or their tier. */
|
|
61
|
+
source: string;
|
|
62
|
+
remainingUsd: number;
|
|
63
|
+
};
|
|
64
|
+
/** Why the owner's jobs may not run at all. */
|
|
65
|
+
export type JobAllowanceRefused = {
|
|
66
|
+
perDayUsd: 0;
|
|
67
|
+
source: null;
|
|
68
|
+
/** The reason in a word, such as a tier that does not include jobs. */
|
|
69
|
+
refused: string;
|
|
70
|
+
/** The same thing in a sentence, for showing a user. */
|
|
71
|
+
message: string;
|
|
72
|
+
};
|
|
73
|
+
export type JobAllowance = JobAllowanceGranted | JobAllowanceRefused;
|
|
74
|
+
/** Whether an allowance is a refusal rather than a grant. */
|
|
75
|
+
export declare function isJobAllowanceRefused(allowance: JobAllowance): allowance is JobAllowanceRefused;
|
|
76
|
+
export type JobPlanPhaseKind = "plan" | "work" | "review";
|
|
77
|
+
export type JobPlanPhaseStatus = "pending" | "running" | "blocked" | "done";
|
|
78
|
+
/** One phase of a job's plan, as the plan card spells it out. */
|
|
79
|
+
export type JobPlanPhase = {
|
|
80
|
+
id: string;
|
|
81
|
+
kind: JobPlanPhaseKind;
|
|
82
|
+
/** The model this phase is run on. */
|
|
83
|
+
model: string;
|
|
84
|
+
status: JobPlanPhaseStatus;
|
|
85
|
+
/** The card this phase has to leave behind before it may be done. */
|
|
86
|
+
artifact?: string;
|
|
87
|
+
/** What finishing it means, in the plan's own words. */
|
|
88
|
+
acceptance?: string;
|
|
89
|
+
/** Phase ids that have to be done first. */
|
|
90
|
+
dependsOn?: string[];
|
|
91
|
+
/** What the shift working this phase is told. */
|
|
92
|
+
brief?: string;
|
|
93
|
+
};
|
|
94
|
+
/**
|
|
95
|
+
* What is wrong with a plan card that could not be read.
|
|
96
|
+
*
|
|
97
|
+
* The parse issue names the line and what is wrong with it; a plain string is what a server
|
|
98
|
+
* that only has a sentence about it sends.
|
|
99
|
+
*/
|
|
100
|
+
export type JobPlanIssue = string | {
|
|
101
|
+
line: number;
|
|
102
|
+
message: string;
|
|
103
|
+
};
|
|
104
|
+
export type JobPlanRead = {
|
|
105
|
+
ok: true;
|
|
106
|
+
raw: string;
|
|
107
|
+
phases: JobPlanPhase[];
|
|
108
|
+
} | {
|
|
109
|
+
ok: false;
|
|
110
|
+
reason: string;
|
|
111
|
+
issue?: JobPlanIssue;
|
|
112
|
+
};
|
|
113
|
+
/**
|
|
114
|
+
* One line of a job's journal.
|
|
115
|
+
*
|
|
116
|
+
* Loosely typed on purpose: the journal is a card the model writes as well as the runtime, and
|
|
117
|
+
* a reader that dropped the keys it did not know would hide exactly the entries worth reading.
|
|
118
|
+
*/
|
|
119
|
+
export type JobJournalEntry = {
|
|
120
|
+
at: number;
|
|
121
|
+
kind?: string;
|
|
122
|
+
title?: string;
|
|
123
|
+
body?: string;
|
|
124
|
+
[key: string]: unknown;
|
|
125
|
+
};
|
|
126
|
+
/** What may be written to the owner's job settings. */
|
|
127
|
+
export type JobSettingsUpdate = {
|
|
128
|
+
/** A flat daily allowance, in USD. */
|
|
129
|
+
allowanceUsd?: number | null;
|
|
130
|
+
/** A daily allowance as a percentage of the owner's usage limit. */
|
|
131
|
+
allowancePercent?: number | null;
|
|
132
|
+
/** The autonomy new jobs start with. */
|
|
133
|
+
autonomy?: JobAutonomy | null;
|
|
134
|
+
/** Things every job of theirs asks about. */
|
|
135
|
+
alwaysAsk?: string[] | null;
|
|
136
|
+
};
|
|
137
|
+
/**
|
|
138
|
+
* The owner's job settings as they now stand.
|
|
139
|
+
*
|
|
140
|
+
* Open on purpose: the settings pipeline owns this document, and a setting it grows is worth
|
|
141
|
+
* handing back rather than dropping because the SDK has not been rebuilt.
|
|
142
|
+
*/
|
|
143
|
+
export type JobSettings = JobSettingsUpdate & {
|
|
144
|
+
[key: string]: unknown;
|
|
145
|
+
};
|
|
146
|
+
export type JobListResponse = {
|
|
147
|
+
success: true;
|
|
148
|
+
jobs: JobView[];
|
|
149
|
+
page: number;
|
|
150
|
+
limit: number;
|
|
151
|
+
total: number;
|
|
152
|
+
/** The status the listing was filtered to, when it was. */
|
|
153
|
+
status?: JobStatus;
|
|
154
|
+
allowance: JobAllowance;
|
|
155
|
+
spentTodayUsd: number;
|
|
156
|
+
};
|
|
157
|
+
export type JobDetailResponse = {
|
|
158
|
+
success: true;
|
|
159
|
+
job: JobView;
|
|
160
|
+
/** The job's autonomy in a sentence, ready to show. */
|
|
161
|
+
autonomyLine: string;
|
|
162
|
+
plan: JobPlanRead;
|
|
163
|
+
journal: JobJournalEntry[];
|
|
164
|
+
/** Questions of this job's nobody has answered yet. */
|
|
165
|
+
openDeliveries: Delivery[];
|
|
166
|
+
};
|
|
167
|
+
export type JobCancelResponse = {
|
|
168
|
+
success: true;
|
|
169
|
+
job: JobView;
|
|
170
|
+
};
|
|
171
|
+
export type JobUpdateResponse = {
|
|
172
|
+
success: true;
|
|
173
|
+
job: JobView;
|
|
174
|
+
autonomyLine: string;
|
|
175
|
+
};
|
|
176
|
+
export type JobSettingsResponse = {
|
|
177
|
+
success: true;
|
|
178
|
+
jobs: JobSettings;
|
|
179
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.JOB_AUTONOMY_MODES = exports.TERMINAL_JOB_STATUSES = exports.JOB_STATUSES = void 0;
|
|
4
|
+
exports.isJobAllowanceRefused = isJobAllowanceRefused;
|
|
5
|
+
/**
|
|
6
|
+
* A job: long-running work Alfred does on its own, run as a sequence of short shifts.
|
|
7
|
+
*
|
|
8
|
+
* These are the shapes the jobs routes answer with. The server flattens a job row into a
|
|
9
|
+
* `JobView` before it leaves, and this is that view rather than the row.
|
|
10
|
+
*/
|
|
11
|
+
exports.JOB_STATUSES = [
|
|
12
|
+
"queued",
|
|
13
|
+
"running",
|
|
14
|
+
"blocked",
|
|
15
|
+
"waiting_user",
|
|
16
|
+
"waiting_approval",
|
|
17
|
+
"waiting_child",
|
|
18
|
+
"waiting_budget",
|
|
19
|
+
"review",
|
|
20
|
+
"done",
|
|
21
|
+
"failed",
|
|
22
|
+
"cancelled",
|
|
23
|
+
];
|
|
24
|
+
/** The statuses a job never leaves. */
|
|
25
|
+
exports.TERMINAL_JOB_STATUSES = ["done", "failed", "cancelled"];
|
|
26
|
+
exports.JOB_AUTONOMY_MODES = ["ask", "free", "auto"];
|
|
27
|
+
/** Whether an allowance is a refusal rather than a grant. */
|
|
28
|
+
function isJobAllowanceRefused(allowance) {
|
|
29
|
+
return allowance.source === null;
|
|
30
|
+
}
|