@butlerbot/sdk 0.0.18-alpha.3 → 0.0.18
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/index.d.ts +10 -0
- package/dist/index.js +27 -25
- package/dist/link/hook.d.ts +70 -0
- package/dist/link/hook.js +60 -0
- package/dist/link/index.d.ts +11 -0
- package/dist/link/index.js +15 -0
- package/dist/link/link.d.ts +142 -0
- package/dist/link/link.js +427 -0
- package/dist/link/protocol.d.ts +186 -0
- package/dist/link/protocol.js +25 -0
- package/dist/link/schema.d.ts +80 -0
- package/dist/link/schema.js +88 -0
- package/dist/link/socket.d.ts +33 -0
- package/dist/link/socket.js +70 -0
- package/dist/link/tool.d.ts +95 -0
- package/dist/link/tool.js +52 -0
- package/dist/modules/conversation.d.ts +28 -9
- package/dist/modules/conversation.js +104 -122
- package/dist/modules/transport.d.ts +101 -0
- package/dist/modules/transport.js +78 -0
- package/dist/modules/transport_link.d.ts +28 -0
- package/dist/modules/transport_link.js +143 -0
- package/dist/modules/transport_sse.d.ts +28 -0
- package/dist/modules/transport_sse.js +58 -0
- package/dist/modules/usage.js +16 -27
- package/dist/util/emitter.d.ts +15 -0
- package/dist/util/emitter.js +51 -0
- package/dist/util/url_formatter.js +4 -1
- package/package.json +34 -10
- package/readme.md +126 -1
|
@@ -1,23 +1,15 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
-
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
-
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
-
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
-
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
-
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
-
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
-
});
|
|
10
|
-
};
|
|
11
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
3
|
exports.Conversation = void 0;
|
|
13
|
-
const eventsource_1 = require("eventsource");
|
|
14
4
|
const config_1 = require("../config");
|
|
5
|
+
const emitter_1 = require("../util/emitter");
|
|
6
|
+
const transport_link_1 = require("./transport_link");
|
|
7
|
+
const transport_sse_1 = require("./transport_sse");
|
|
15
8
|
const url_formatter_1 = require("../util/url_formatter");
|
|
16
9
|
const DEFAULT_CONVO_API_V = "v4";
|
|
17
10
|
class Conversation {
|
|
18
11
|
constructor(config) {
|
|
19
|
-
|
|
20
|
-
this.events = new Map();
|
|
12
|
+
this.events = new emitter_1.Emitter();
|
|
21
13
|
this.convoId = config.convoId;
|
|
22
14
|
this.chatApiV = (config.chatApiV || DEFAULT_CONVO_API_V);
|
|
23
15
|
const serverUrl = config.serverUrl || config_1.CONFIG.server;
|
|
@@ -25,11 +17,27 @@ class Conversation {
|
|
|
25
17
|
this.endpoints = {
|
|
26
18
|
conversation: serverUrl + (config.convoPath || config.path || config_1.CONFIG.paths.conversation[this.chatApiV].base),
|
|
27
19
|
history: serverUrl + (config.historyPath || config_1.CONFIG.paths.history.chat.v1.base),
|
|
28
|
-
progressStream: serverUrl + (config.progressStreamPath || (
|
|
29
|
-
progress: serverUrl + (config.progressPath || (
|
|
20
|
+
progressStream: serverUrl + (config.progressStreamPath || (progressConfig?.stream ?? config_1.CONFIG.paths.progress.v4.stream)),
|
|
21
|
+
progress: serverUrl + (config.progressPath || (progressConfig?.base ?? config_1.CONFIG.paths.progress.v4.base)),
|
|
30
22
|
};
|
|
31
23
|
this.apiKey = config.apiKey;
|
|
32
24
|
this.debug = config.debug || false;
|
|
25
|
+
if (config.transport && config.transport !== "sse") {
|
|
26
|
+
this.link = config.transport;
|
|
27
|
+
this.transport = new transport_link_1.LinkConversationTransport(config.transport, () => ({
|
|
28
|
+
model: this.options?.model,
|
|
29
|
+
personality: this.options?.personality,
|
|
30
|
+
instructions: this.options?.instructions,
|
|
31
|
+
platform: this.options?.platform,
|
|
32
|
+
}));
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
this.transport = new transport_sse_1.SSEConversationTransport({
|
|
36
|
+
endpoint: () => this.endpoints.conversation,
|
|
37
|
+
apiKey: this.apiKey,
|
|
38
|
+
debug: this.debug,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
33
41
|
}
|
|
34
42
|
// SETTERS
|
|
35
43
|
/**
|
|
@@ -67,22 +75,22 @@ class Conversation {
|
|
|
67
75
|
}
|
|
68
76
|
/** Sets the AI model used for the next interaction, e.g Claude-Sonnet, GPT-4 */
|
|
69
77
|
setModel(model) {
|
|
70
|
-
this.options =
|
|
78
|
+
this.options = { ...this.options, model };
|
|
71
79
|
return this;
|
|
72
80
|
}
|
|
73
81
|
/** Sets additional instructions for location-specific context */
|
|
74
82
|
setInstructions(instructions) {
|
|
75
|
-
this.options =
|
|
83
|
+
this.options = { ...this.options, instructions };
|
|
76
84
|
return this;
|
|
77
85
|
}
|
|
78
86
|
/** Sets the platform where the chat is occurring, used internally for logging - ignore in most contexts */
|
|
79
87
|
setPlatform(platform) {
|
|
80
|
-
this.options =
|
|
88
|
+
this.options = { ...this.options, platform };
|
|
81
89
|
return this;
|
|
82
90
|
}
|
|
83
91
|
/** Sets a custom personality configuration for the AI */
|
|
84
92
|
setPersonality(personality) {
|
|
85
|
-
this.options =
|
|
93
|
+
this.options = { ...this.options, personality };
|
|
86
94
|
return this;
|
|
87
95
|
}
|
|
88
96
|
// GETTERS
|
|
@@ -115,53 +123,21 @@ class Conversation {
|
|
|
115
123
|
}
|
|
116
124
|
/** Gets the current options object */
|
|
117
125
|
getModel() {
|
|
118
|
-
|
|
119
|
-
return (_a = this.options) === null || _a === void 0 ? void 0 : _a.model;
|
|
126
|
+
return this.options?.model;
|
|
120
127
|
}
|
|
121
128
|
/** Gets the current location-specific instructions */
|
|
122
129
|
getInstructions() {
|
|
123
|
-
|
|
124
|
-
return (_a = this.options) === null || _a === void 0 ? void 0 : _a.instructions;
|
|
130
|
+
return this.options?.instructions;
|
|
125
131
|
}
|
|
126
132
|
/** Gets the current platform */
|
|
127
133
|
getPlatform() {
|
|
128
|
-
|
|
129
|
-
return (_a = this.options) === null || _a === void 0 ? void 0 : _a.platform;
|
|
134
|
+
return this.options?.platform;
|
|
130
135
|
}
|
|
131
136
|
/** Gets the current personality configuration */
|
|
132
137
|
getPersonality() {
|
|
133
|
-
|
|
134
|
-
return (_a = this.options) === null || _a === void 0 ? void 0 : _a.personality;
|
|
138
|
+
return this.options?.personality;
|
|
135
139
|
}
|
|
136
140
|
// EVENT EMITTER
|
|
137
|
-
hasEmitter(event) {
|
|
138
|
-
return this.events.has(event);
|
|
139
|
-
}
|
|
140
|
-
addEmitter(event) {
|
|
141
|
-
if (!this.hasEmitter(event))
|
|
142
|
-
this.events.set(event, new Map());
|
|
143
|
-
}
|
|
144
|
-
removeEmitter(event) {
|
|
145
|
-
this.events.delete(event);
|
|
146
|
-
}
|
|
147
|
-
addListener(event, cb) {
|
|
148
|
-
if (!this.hasEmitter(event))
|
|
149
|
-
this.addEmitter(event);
|
|
150
|
-
const listeners = this.events.get(event);
|
|
151
|
-
const id = crypto.randomUUID();
|
|
152
|
-
listeners.set(id, cb);
|
|
153
|
-
return id;
|
|
154
|
-
}
|
|
155
|
-
removeListener(event, id) {
|
|
156
|
-
const listeners = this.events.get(event);
|
|
157
|
-
if (listeners)
|
|
158
|
-
listeners.delete(id);
|
|
159
|
-
}
|
|
160
|
-
emit(event, ...args) {
|
|
161
|
-
const listeners = this.events.get(event);
|
|
162
|
-
if (listeners)
|
|
163
|
-
listeners.forEach(listener => listener(...args));
|
|
164
|
-
}
|
|
165
141
|
/**
|
|
166
142
|
* Fires when the conversation ID is set
|
|
167
143
|
* if convoId is already set when this is called, fires immediately
|
|
@@ -169,11 +145,11 @@ class Conversation {
|
|
|
169
145
|
onConvoId(cb) {
|
|
170
146
|
if (this.convoId)
|
|
171
147
|
cb(this.convoId);
|
|
172
|
-
return this.
|
|
148
|
+
return this.events.on("convoId", cb);
|
|
173
149
|
}
|
|
174
150
|
/** Removes a convoId listener */
|
|
175
151
|
offConvoId(listenerId) {
|
|
176
|
-
this.
|
|
152
|
+
this.events.off("convoId", listenerId);
|
|
177
153
|
}
|
|
178
154
|
/**
|
|
179
155
|
* Fires once when the conversation ID is set, then removes the listener.
|
|
@@ -184,91 +160,97 @@ class Conversation {
|
|
|
184
160
|
cb(this.convoId);
|
|
185
161
|
return;
|
|
186
162
|
}
|
|
187
|
-
|
|
188
|
-
cb(convoId);
|
|
189
|
-
this.offConvoId(id);
|
|
190
|
-
});
|
|
191
|
-
return id;
|
|
192
|
-
}
|
|
193
|
-
// SSE HANDLER
|
|
194
|
-
handleSSE(url, cb, options = {}) {
|
|
195
|
-
const sse = new eventsource_1.EventSource(url);
|
|
196
|
-
sse.addEventListener("message", (event) => {
|
|
197
|
-
const data = JSON.parse(event.data);
|
|
198
|
-
const convoId = data.success ? data.data.convoId : undefined;
|
|
199
|
-
if (convoId && options.onConvoId)
|
|
200
|
-
options.onConvoId(convoId);
|
|
201
|
-
cb(data);
|
|
202
|
-
if (data.data.quitStream)
|
|
203
|
-
sse.close();
|
|
204
|
-
});
|
|
205
|
-
sse.addEventListener("error", (event) => {
|
|
206
|
-
if (this.debug)
|
|
207
|
-
console.warn(`[Stream Error: ${url}]`, event);
|
|
208
|
-
});
|
|
209
|
-
return sse;
|
|
163
|
+
return this.events.once("convoId", cb);
|
|
210
164
|
}
|
|
211
165
|
// GETTERS
|
|
212
166
|
/** Fetches the conversation state from the server, including message history and metadata */
|
|
213
|
-
fetchState() {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
return data;
|
|
225
|
-
});
|
|
167
|
+
async fetchState() {
|
|
168
|
+
if (!this.convoId)
|
|
169
|
+
throw new Error("Conversation ID is not set");
|
|
170
|
+
const url = (0, url_formatter_1.formatURL)(`${this.endpoints.history}/${this.convoId}`, undefined, { apiKey: this.apiKey, debug: this.debug });
|
|
171
|
+
const response = await fetch(url);
|
|
172
|
+
if (!response.ok) {
|
|
173
|
+
const errorText = await response.text();
|
|
174
|
+
throw new Error(`Failed to fetch conversation state: ${response.status} ${response.statusText} - ${errorText}`);
|
|
175
|
+
}
|
|
176
|
+
const data = await response.json();
|
|
177
|
+
return data;
|
|
226
178
|
}
|
|
227
179
|
/** Fetches the conversation progress stream from the server */
|
|
228
180
|
fetchProgressStream(cb) {
|
|
229
181
|
if (!this.convoId)
|
|
230
182
|
throw new Error("Conversation ID is not set");
|
|
231
183
|
const url = (0, url_formatter_1.formatURL)(this.endpoints.progressStream, { chatId: this.convoId }, { apiKey: this.apiKey, debug: this.debug });
|
|
232
|
-
return
|
|
184
|
+
return (0, transport_sse_1.streamSSE)(url, { debug: this.debug, onPayload: (payload) => cb(payload) });
|
|
233
185
|
}
|
|
234
186
|
/**
|
|
235
187
|
* Fetches the conversation progress from the server
|
|
236
188
|
* Returns undefined if no active turn progress
|
|
237
189
|
*/
|
|
238
|
-
fetchProgress(options) {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
return data.events;
|
|
257
|
-
});
|
|
190
|
+
async fetchProgress(options) {
|
|
191
|
+
if (!this.convoId)
|
|
192
|
+
throw new Error("Conversation ID is not set");
|
|
193
|
+
const payload = {
|
|
194
|
+
chatId: this.convoId,
|
|
195
|
+
};
|
|
196
|
+
if (options?.includeCompleted !== undefined)
|
|
197
|
+
payload["includeCompleted"] = options.includeCompleted;
|
|
198
|
+
const url = (0, url_formatter_1.formatURL)(this.endpoints.progress, payload, { apiKey: this.apiKey, debug: this.debug });
|
|
199
|
+
const response = await fetch(url, { headers: options?.lastEventId ? { "last-event-id": options.lastEventId } : undefined });
|
|
200
|
+
if (!response.ok) {
|
|
201
|
+
const errorText = await response.text();
|
|
202
|
+
throw new Error(`Failed to fetch conversation progress: ${response.status} ${response.statusText} - ${errorText}`);
|
|
203
|
+
}
|
|
204
|
+
if (response.status === 204)
|
|
205
|
+
return undefined; // No content
|
|
206
|
+
const data = await response.json();
|
|
207
|
+
return data.events;
|
|
258
208
|
}
|
|
259
209
|
// LIFE CYCLE
|
|
260
210
|
/** Sends a message into the conversation */
|
|
261
211
|
send(message, cb, options) {
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
212
|
+
return this.transport.send({
|
|
213
|
+
message,
|
|
214
|
+
...this.options, // options set for convo
|
|
215
|
+
...options, // overwrite convo's for this call
|
|
216
|
+
...(this.convoId ? { chatId: this.convoId } : {}),
|
|
217
|
+
}, {
|
|
218
|
+
payload: (payload) => cb(payload),
|
|
219
|
+
convoId: (convoId) => {
|
|
220
|
+
if (this.convoId === convoId)
|
|
221
|
+
return;
|
|
269
222
|
this.convoId = convoId;
|
|
270
|
-
this.emit("convoId", convoId);
|
|
271
|
-
}
|
|
223
|
+
this.events.emit("convoId", convoId);
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Sends a message and resolves with the finished reply.
|
|
229
|
+
*
|
|
230
|
+
* For when you want the answer rather than the stream. Every event still arrives
|
|
231
|
+
* through `onEvent` if you pass one.
|
|
232
|
+
*/
|
|
233
|
+
ask(message, options) {
|
|
234
|
+
return new Promise((resolve, reject) => {
|
|
235
|
+
const events = [];
|
|
236
|
+
let text = "";
|
|
237
|
+
this.send(message, (chunk) => {
|
|
238
|
+
events.push(chunk);
|
|
239
|
+
options?.onEvent?.(chunk);
|
|
240
|
+
if (!chunk.success) {
|
|
241
|
+
reject(new Error(chunk.data.message || chunk.data.error || chunk.data.code));
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
const response = chunk.data.response;
|
|
245
|
+
const metadata = chunk.data.response.metadata;
|
|
246
|
+
// Message chunks arrive cumulative, and Alfred's own notices are not part
|
|
247
|
+
// of the reply, so they are collected but not concatenated into it.
|
|
248
|
+
if (response.type === "message" && metadata?.participantId !== "system" && response.payload?.message) {
|
|
249
|
+
text = response.payload.message;
|
|
250
|
+
}
|
|
251
|
+
if (chunk.data.quitStream)
|
|
252
|
+
resolve({ text, convoId: this.convoId, events });
|
|
253
|
+
}, options);
|
|
272
254
|
});
|
|
273
255
|
}
|
|
274
256
|
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CONVERSATION TRANSPORTS
|
|
3
|
+
* =======================
|
|
4
|
+
*
|
|
5
|
+
* A conversation is the same conversation however it is carried. The transport
|
|
6
|
+
* only decides how a turn is sent and how its stream comes back — everything a
|
|
7
|
+
* caller sees, including the payload shape, is identical either way.
|
|
8
|
+
*/
|
|
9
|
+
/** A turn in progress. `close()` stops delivery locally. */
|
|
10
|
+
export type ConversationStream = {
|
|
11
|
+
/**
|
|
12
|
+
* Stops listening. The turn itself continues server-side and its reply is still
|
|
13
|
+
* persisted, on both transports — there is no cancel.
|
|
14
|
+
*/
|
|
15
|
+
close(): void;
|
|
16
|
+
/** The underlying EventSource, when the turn is being carried over SSE. */
|
|
17
|
+
readonly source?: unknown;
|
|
18
|
+
};
|
|
19
|
+
export type TransportTurnRequest = {
|
|
20
|
+
chatId?: string;
|
|
21
|
+
message: string;
|
|
22
|
+
model?: string;
|
|
23
|
+
instructions?: string;
|
|
24
|
+
platform?: string;
|
|
25
|
+
personality?: string;
|
|
26
|
+
};
|
|
27
|
+
export type TransportHandlers = {
|
|
28
|
+
/** One payload of the stream, already in the shape callers expect. */
|
|
29
|
+
payload(payload: unknown): void;
|
|
30
|
+
/** The conversation this turn belongs to, as soon as it is known. */
|
|
31
|
+
convoId(convoId: string): void;
|
|
32
|
+
};
|
|
33
|
+
export interface ConversationTransport {
|
|
34
|
+
send(request: TransportTurnRequest, handlers: TransportHandlers): ConversationStream;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* The system message form the HTTP transport uses for a notice.
|
|
38
|
+
*
|
|
39
|
+
* Alfred's own remarks — a forced model switch, a tier limit — are not part of
|
|
40
|
+
* conversation state and won't be in history. Both transports present them the same
|
|
41
|
+
* way so a consumer needs no special case.
|
|
42
|
+
*/
|
|
43
|
+
export declare function noticePayload(message: string, convoId?: string): {
|
|
44
|
+
success: true;
|
|
45
|
+
data: {
|
|
46
|
+
convoId?: string | undefined;
|
|
47
|
+
response: {
|
|
48
|
+
type: string;
|
|
49
|
+
payload: {
|
|
50
|
+
message: string;
|
|
51
|
+
messageId: string;
|
|
52
|
+
completed: boolean;
|
|
53
|
+
};
|
|
54
|
+
metadata: {
|
|
55
|
+
responseId: string;
|
|
56
|
+
model: string;
|
|
57
|
+
modelId: string;
|
|
58
|
+
timestamp: number;
|
|
59
|
+
firstTimestamp: number;
|
|
60
|
+
participantId: string;
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
/** The first payload of a turn: which conversation it landed in. */
|
|
66
|
+
export declare function convoStartedPayload(convoId: string): {
|
|
67
|
+
success: true;
|
|
68
|
+
data: {
|
|
69
|
+
response: {
|
|
70
|
+
type: string;
|
|
71
|
+
payload: {
|
|
72
|
+
state: string;
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
convoId: string;
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
/** The terminal payload of a turn that produced no completion event of its own. */
|
|
79
|
+
export declare function completedPayload(convoId?: string): {
|
|
80
|
+
success: true;
|
|
81
|
+
data: {
|
|
82
|
+
quitStream: boolean;
|
|
83
|
+
convoId?: string | undefined;
|
|
84
|
+
response: {
|
|
85
|
+
type: string;
|
|
86
|
+
payload: {
|
|
87
|
+
completed: boolean;
|
|
88
|
+
};
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
};
|
|
92
|
+
export declare function failurePayload(code: string, error: string, message: string, convoId?: string): {
|
|
93
|
+
success: false;
|
|
94
|
+
data: {
|
|
95
|
+
quitStream: boolean;
|
|
96
|
+
convoId?: string | undefined;
|
|
97
|
+
code: string;
|
|
98
|
+
error: string;
|
|
99
|
+
message: string;
|
|
100
|
+
};
|
|
101
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* CONVERSATION TRANSPORTS
|
|
4
|
+
* =======================
|
|
5
|
+
*
|
|
6
|
+
* A conversation is the same conversation however it is carried. The transport
|
|
7
|
+
* only decides how a turn is sent and how its stream comes back — everything a
|
|
8
|
+
* caller sees, including the payload shape, is identical either way.
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.noticePayload = noticePayload;
|
|
12
|
+
exports.convoStartedPayload = convoStartedPayload;
|
|
13
|
+
exports.completedPayload = completedPayload;
|
|
14
|
+
exports.failurePayload = failurePayload;
|
|
15
|
+
// =============================================
|
|
16
|
+
// PAYLOAD SHAPES
|
|
17
|
+
// =============================================
|
|
18
|
+
/**
|
|
19
|
+
* The system message form the HTTP transport uses for a notice.
|
|
20
|
+
*
|
|
21
|
+
* Alfred's own remarks — a forced model switch, a tier limit — are not part of
|
|
22
|
+
* conversation state and won't be in history. Both transports present them the same
|
|
23
|
+
* way so a consumer needs no special case.
|
|
24
|
+
*/
|
|
25
|
+
function noticePayload(message, convoId) {
|
|
26
|
+
const messageId = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
27
|
+
return {
|
|
28
|
+
success: true,
|
|
29
|
+
data: {
|
|
30
|
+
response: {
|
|
31
|
+
type: "message",
|
|
32
|
+
payload: { message, messageId, completed: true },
|
|
33
|
+
metadata: {
|
|
34
|
+
responseId: messageId,
|
|
35
|
+
model: "System",
|
|
36
|
+
modelId: "system",
|
|
37
|
+
timestamp: Date.now(),
|
|
38
|
+
firstTimestamp: Date.now(),
|
|
39
|
+
participantId: "system",
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
...(convoId ? { convoId } : {}),
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/** The first payload of a turn: which conversation it landed in. */
|
|
47
|
+
function convoStartedPayload(convoId) {
|
|
48
|
+
return {
|
|
49
|
+
success: true,
|
|
50
|
+
data: {
|
|
51
|
+
response: { type: "convo_status", payload: { state: "started" } },
|
|
52
|
+
convoId,
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/** The terminal payload of a turn that produced no completion event of its own. */
|
|
57
|
+
function completedPayload(convoId) {
|
|
58
|
+
return {
|
|
59
|
+
success: true,
|
|
60
|
+
data: {
|
|
61
|
+
response: { type: "response_status", payload: { completed: true } },
|
|
62
|
+
...(convoId ? { convoId } : {}),
|
|
63
|
+
quitStream: true,
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function failurePayload(code, error, message, convoId) {
|
|
68
|
+
return {
|
|
69
|
+
success: false,
|
|
70
|
+
data: {
|
|
71
|
+
code,
|
|
72
|
+
error,
|
|
73
|
+
message,
|
|
74
|
+
...(convoId ? { convoId } : {}),
|
|
75
|
+
quitStream: true,
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Link } from "../link/link";
|
|
2
|
+
import { ConversationStream, ConversationTransport, TransportHandlers, TransportTurnRequest } from "./transport";
|
|
3
|
+
export type LinkSessionConfig = {
|
|
4
|
+
model?: string;
|
|
5
|
+
personality?: string;
|
|
6
|
+
instructions?: string;
|
|
7
|
+
platform?: string;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Carries a turn over an existing Link connection.
|
|
11
|
+
*
|
|
12
|
+
* Sessions are ephemeral: the server drops them when the socket goes, so one is
|
|
13
|
+
* opened on demand and reopened transparently after a reconnect. The conversation
|
|
14
|
+
* itself is persisted server-side, so nothing is lost when that happens.
|
|
15
|
+
*/
|
|
16
|
+
export declare class LinkConversationTransport implements ConversationTransport {
|
|
17
|
+
private readonly link;
|
|
18
|
+
private readonly config;
|
|
19
|
+
private sessionId?;
|
|
20
|
+
private sessionChatId?;
|
|
21
|
+
constructor(link: Link, config: () => LinkSessionConfig);
|
|
22
|
+
send(request: TransportTurnRequest, handlers: TransportHandlers): ConversationStream;
|
|
23
|
+
/** Ends the session, if one is open. The conversation can still be resumed later. */
|
|
24
|
+
end(): Promise<void>;
|
|
25
|
+
private runTurn;
|
|
26
|
+
/** Opens a session, or reuses the open one when it is for the same conversation. */
|
|
27
|
+
private session;
|
|
28
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LinkConversationTransport = void 0;
|
|
4
|
+
const protocol_1 = require("../link/protocol");
|
|
5
|
+
const transport_1 = require("./transport");
|
|
6
|
+
/**
|
|
7
|
+
* Carries a turn over an existing Link connection.
|
|
8
|
+
*
|
|
9
|
+
* Sessions are ephemeral: the server drops them when the socket goes, so one is
|
|
10
|
+
* opened on demand and reopened transparently after a reconnect. The conversation
|
|
11
|
+
* itself is persisted server-side, so nothing is lost when that happens.
|
|
12
|
+
*/
|
|
13
|
+
class LinkConversationTransport {
|
|
14
|
+
constructor(link, config) {
|
|
15
|
+
this.link = link;
|
|
16
|
+
this.config = config;
|
|
17
|
+
// A session cannot outlive the connection it was opened on.
|
|
18
|
+
link.on("disconnect", () => { this.sessionId = undefined; });
|
|
19
|
+
}
|
|
20
|
+
send(request, handlers) {
|
|
21
|
+
let closed = false;
|
|
22
|
+
const deliver = {
|
|
23
|
+
payload: (payload) => { if (!closed)
|
|
24
|
+
handlers.payload(payload); },
|
|
25
|
+
convoId: (convoId) => { if (!closed)
|
|
26
|
+
handlers.convoId(convoId); },
|
|
27
|
+
};
|
|
28
|
+
void this.runTurn(request, deliver, true).catch((error) => {
|
|
29
|
+
const failure = error instanceof protocol_1.LinkError
|
|
30
|
+
? (0, transport_1.failurePayload)(error.code, error.message, error.message, request.chatId)
|
|
31
|
+
: (0, transport_1.failurePayload)("link_error", String(error), "I'm afraid the connection to Alfred failed.", request.chatId);
|
|
32
|
+
deliver.payload(failure);
|
|
33
|
+
});
|
|
34
|
+
return { close: () => { closed = true; } };
|
|
35
|
+
}
|
|
36
|
+
/** Ends the session, if one is open. The conversation can still be resumed later. */
|
|
37
|
+
async end() {
|
|
38
|
+
const sessionId = this.sessionId;
|
|
39
|
+
if (!sessionId)
|
|
40
|
+
return;
|
|
41
|
+
this.sessionId = undefined;
|
|
42
|
+
await this.link.exchange("conversation.end", { sessionId });
|
|
43
|
+
}
|
|
44
|
+
async runTurn(request, handlers, mayRetry) {
|
|
45
|
+
const sessionId = await this.session(request);
|
|
46
|
+
let chatId = request.chatId ?? this.sessionChatId;
|
|
47
|
+
let announcedChatId = false;
|
|
48
|
+
let sawCompletion = false;
|
|
49
|
+
const learnChatId = (candidate) => {
|
|
50
|
+
if (!candidate)
|
|
51
|
+
return;
|
|
52
|
+
chatId = candidate;
|
|
53
|
+
this.sessionChatId = candidate;
|
|
54
|
+
handlers.convoId(candidate);
|
|
55
|
+
// Mirrors the HTTP transport's first byte, which tells a client which
|
|
56
|
+
// conversation it is now in. A brand new conversation only has an id once
|
|
57
|
+
// its first turn has begun.
|
|
58
|
+
if (!announcedChatId) {
|
|
59
|
+
announcedChatId = true;
|
|
60
|
+
handlers.payload((0, transport_1.convoStartedPayload)(candidate));
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
learnChatId(chatId);
|
|
64
|
+
const done = await this.link.exchange("conversation.chat", {
|
|
65
|
+
sessionId,
|
|
66
|
+
message: request.message,
|
|
67
|
+
...(request.model ? { model: request.model } : {}),
|
|
68
|
+
...(request.instructions ? { instructions: request.instructions } : {}),
|
|
69
|
+
...(request.personality ? { personality: request.personality } : {}),
|
|
70
|
+
}, {
|
|
71
|
+
// A turn takes as long as it takes; only the transport dying ends it early.
|
|
72
|
+
timeoutMs: 0,
|
|
73
|
+
isDone: (frame) => frame.type === "conversation.done",
|
|
74
|
+
onFrame: (frame) => {
|
|
75
|
+
if (frame.type === "conversation.event") {
|
|
76
|
+
const payload = frame.payload;
|
|
77
|
+
learnChatId(payload.chatId);
|
|
78
|
+
const event = payload.event;
|
|
79
|
+
const final = event.type === "response_status" && Boolean(event.payload?.completed);
|
|
80
|
+
if (final)
|
|
81
|
+
sawCompletion = true;
|
|
82
|
+
handlers.payload({
|
|
83
|
+
success: true,
|
|
84
|
+
data: {
|
|
85
|
+
response: event,
|
|
86
|
+
...(payload.chatId ?? chatId ? { convoId: payload.chatId ?? chatId } : {}),
|
|
87
|
+
...(final ? { quitStream: true } : {}),
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (frame.type === "conversation.notice") {
|
|
93
|
+
const payload = frame.payload;
|
|
94
|
+
learnChatId(payload.chatId);
|
|
95
|
+
handlers.payload((0, transport_1.noticePayload)(payload.message, payload.chatId ?? chatId));
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
const payload = done.payload;
|
|
100
|
+
learnChatId(payload.chatId);
|
|
101
|
+
if (payload.ok) {
|
|
102
|
+
// Nearly always the pipeline's own completion event has already closed the
|
|
103
|
+
// stream; this is for the turn that ended without one.
|
|
104
|
+
if (!sawCompletion)
|
|
105
|
+
handlers.payload((0, transport_1.completedPayload)(payload.chatId ?? chatId));
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
// The session died with a connection we have since replaced. Reopening it is
|
|
109
|
+
// invisible to the caller, and the message has not been delivered yet.
|
|
110
|
+
if (payload.code === "unknown_session" && mayRetry) {
|
|
111
|
+
this.sessionId = undefined;
|
|
112
|
+
await this.runTurn(request, handlers, false);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
handlers.payload((0, transport_1.failurePayload)(
|
|
116
|
+
// `turn_failed` means the dialogue itself failed, and then `error` holds the
|
|
117
|
+
// code the HTTP transport would have reported.
|
|
118
|
+
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 ?? chatId));
|
|
119
|
+
}
|
|
120
|
+
/** Opens a session, or reuses the open one when it is for the same conversation. */
|
|
121
|
+
async session(request) {
|
|
122
|
+
const chatId = request.chatId ?? this.sessionChatId;
|
|
123
|
+
if (this.sessionId && this.sessionChatId === chatId)
|
|
124
|
+
return this.sessionId;
|
|
125
|
+
if (this.sessionId)
|
|
126
|
+
await this.end();
|
|
127
|
+
const config = this.config();
|
|
128
|
+
const opened = await this.link.exchange("conversation.start", {
|
|
129
|
+
...(chatId ? { chatId } : {}),
|
|
130
|
+
...(request.model ?? config.model ? { model: request.model ?? config.model } : {}),
|
|
131
|
+
...(request.personality ?? config.personality ? { personality: request.personality ?? config.personality } : {}),
|
|
132
|
+
...(request.instructions ?? config.instructions ? { instructions: request.instructions ?? config.instructions } : {}),
|
|
133
|
+
...(request.platform ?? config.platform ? { platform: request.platform ?? config.platform } : {}),
|
|
134
|
+
}, {
|
|
135
|
+
isDone: (frame) => frame.type === "conversation.open",
|
|
136
|
+
});
|
|
137
|
+
const payload = opened.payload;
|
|
138
|
+
this.sessionId = payload.sessionId;
|
|
139
|
+
this.sessionChatId = payload.chatId ?? chatId;
|
|
140
|
+
return payload.sessionId;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
exports.LinkConversationTransport = LinkConversationTransport;
|