@butlerbot/sdk 0.0.29 → 0.0.31
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 +10 -0
- package/dist/config.js +10 -0
- package/dist/index.d.ts +1 -0
- package/dist/link/protocol.d.ts +30 -0
- package/dist/modules/conversation.d.ts +27 -1
- package/dist/modules/conversation.js +43 -0
- package/dist/modules/transport.d.ts +57 -2
- package/dist/modules/transport_link.d.ts +16 -1
- package/dist/modules/transport_link.js +43 -0
- package/dist/modules/transport_sse.d.ts +13 -1
- package/dist/modules/transport_sse.js +62 -0
- package/dist/types/conversation/v3/conversation_v3.d.ts +10 -0
- package/package.json +1 -1
package/dist/config.d.ts
CHANGED
package/dist/config.js
CHANGED
|
@@ -18,6 +18,16 @@ exports.CONFIG = {
|
|
|
18
18
|
v1: { base: "/api/convo/get/history" }
|
|
19
19
|
}
|
|
20
20
|
},
|
|
21
|
+
interrupt: {
|
|
22
|
+
v4: {
|
|
23
|
+
stop: "/api/alfred/v4/chat/stop",
|
|
24
|
+
steer: "/api/alfred/v4/chat/steer",
|
|
25
|
+
},
|
|
26
|
+
v5: {
|
|
27
|
+
stop: "/api/alfred/v5/chat/stop",
|
|
28
|
+
steer: "/api/alfred/v5/chat/steer",
|
|
29
|
+
},
|
|
30
|
+
},
|
|
21
31
|
progress: {
|
|
22
32
|
v4: {
|
|
23
33
|
base: "/api/alfred/v4/chat/progress",
|
package/dist/index.d.ts
CHANGED
|
@@ -38,3 +38,4 @@ export type { APIPath };
|
|
|
38
38
|
export type { ConversationStream, ConversationTransport, TransportTurnRequest, TransportHandlers, } from "./modules/transport";
|
|
39
39
|
export { LinkConversationTransport } from "./modules/transport_link";
|
|
40
40
|
export { SSEConversationTransport } from "./modules/transport_sse";
|
|
41
|
+
export type { SteerResult, TurnStopMode, TurnStopped } from "./modules/transport";
|
package/dist/link/protocol.d.ts
CHANGED
|
@@ -120,6 +120,25 @@ export type LinkClientPayloads = {
|
|
|
120
120
|
"conversation.detach": {
|
|
121
121
|
chatId: string;
|
|
122
122
|
};
|
|
123
|
+
/**
|
|
124
|
+
* Stops a running turn.
|
|
125
|
+
*
|
|
126
|
+
* `soft` lets the model call in flight finish and takes no further step; `hard` aborts the
|
|
127
|
+
* stream where it stands. A hard stop against a provider that cannot be cancelled is
|
|
128
|
+
* applied as a soft one — the answer says which was applied.
|
|
129
|
+
*/
|
|
130
|
+
"conversation.stop": {
|
|
131
|
+
chatId: string;
|
|
132
|
+
mode: "soft" | "hard";
|
|
133
|
+
};
|
|
134
|
+
/**
|
|
135
|
+
* Says something to a turn that is still running. It reaches the model at its next step
|
|
136
|
+
* boundary; the turn is not interrupted.
|
|
137
|
+
*/
|
|
138
|
+
"conversation.steer": {
|
|
139
|
+
chatId: string;
|
|
140
|
+
message: string;
|
|
141
|
+
};
|
|
123
142
|
};
|
|
124
143
|
export type LinkClientFrameType = keyof LinkClientPayloads;
|
|
125
144
|
/**
|
|
@@ -196,6 +215,17 @@ export type LinkServerPayloads = {
|
|
|
196
215
|
error?: string;
|
|
197
216
|
message?: string;
|
|
198
217
|
};
|
|
218
|
+
/** A turn was stopped. `mode` is what was applied, `requestedMode` what was asked for. */
|
|
219
|
+
"conversation.stopped": {
|
|
220
|
+
chatId: string;
|
|
221
|
+
mode: "soft" | "hard";
|
|
222
|
+
requestedMode: "soft" | "hard";
|
|
223
|
+
by: "user" | "system";
|
|
224
|
+
};
|
|
225
|
+
/** A steered message was accepted and will reach the model at its next step boundary. */
|
|
226
|
+
"conversation.steered": {
|
|
227
|
+
chatId: string;
|
|
228
|
+
};
|
|
199
229
|
"goodbye": {
|
|
200
230
|
reason: string;
|
|
201
231
|
reconnectAfterMs: number;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { EventSource } from "eventsource";
|
|
2
2
|
import { APIPath } from "../config";
|
|
3
3
|
import type { Link } from "../link/link";
|
|
4
|
-
import { ConversationStream } from "./transport";
|
|
4
|
+
import { ConversationStream, SteerResult, TurnStopMode, TurnStopped } from "./transport";
|
|
5
5
|
import { RequestResponseV3, RequestResponseV4 } from "../types/type_registry";
|
|
6
6
|
import { RequestResponseV5 } from "../types/response/v5/dialogue_response_v5";
|
|
7
7
|
import { ConversationStateResponse } from "../types/state/convo_state_response";
|
|
@@ -181,6 +181,32 @@ export declare class Conversation<V extends APIPath = "v4"> {
|
|
|
181
181
|
private receiver;
|
|
182
182
|
/** Sends a message into the conversation */
|
|
183
183
|
send(message: string, cb: (chunk: RequestResponseByVersion[V]) => any, options?: DialogueRequestOptions): ConversationStream;
|
|
184
|
+
/**
|
|
185
|
+
* Stops the turn running in this conversation.
|
|
186
|
+
*
|
|
187
|
+
* `soft` (the default) lets the model finish the call it is in the middle of and takes no
|
|
188
|
+
* further step: the answer so far is kept, in history and in what the model remembers
|
|
189
|
+
* saying, and it works on every model. `hard` cuts the stream off mid-sentence and aborts
|
|
190
|
+
* the tools with it — but only actually stops the bill where the provider supports
|
|
191
|
+
* cancellation, so against one that does not the server applies a soft stop instead. The
|
|
192
|
+
* returned `mode` says which you got; `undefined` means there was nothing left to stop.
|
|
193
|
+
*
|
|
194
|
+
* Either way the turn's stream ends the way it always does, carrying what was produced
|
|
195
|
+
* before the stop. There is nothing else to clean up.
|
|
196
|
+
*/
|
|
197
|
+
stop(mode?: TurnStopMode): Promise<TurnStopped | undefined>;
|
|
198
|
+
/**
|
|
199
|
+
* Says something to the turn that is already running.
|
|
200
|
+
*
|
|
201
|
+
* The message reaches the model at its next step boundary, so nothing in flight is thrown
|
|
202
|
+
* away and the model reads it as the user talking mid-task — which is usually what someone
|
|
203
|
+
* typing while Alfred works actually means.
|
|
204
|
+
*
|
|
205
|
+
* Check the result. `too_late` means the message was NOT delivered, because the turn was
|
|
206
|
+
* already stopping or over, and it should be sent with `send()` instead. Dropping it is
|
|
207
|
+
* the one thing that is never right.
|
|
208
|
+
*/
|
|
209
|
+
steer(message: string): Promise<SteerResult>;
|
|
184
210
|
/**
|
|
185
211
|
* Sends a message and resolves with the finished reply.
|
|
186
212
|
*
|
|
@@ -15,11 +15,14 @@ class Conversation {
|
|
|
15
15
|
this.chatApiV = (config.chatApiV || DEFAULT_CONVO_API_V);
|
|
16
16
|
const serverUrl = config.serverUrl || config_1.CONFIG.server;
|
|
17
17
|
const progressConfig = config_1.CONFIG.paths.progress[this.chatApiV];
|
|
18
|
+
const interruptConfig = config_1.CONFIG.paths.interrupt[this.chatApiV];
|
|
18
19
|
this.endpoints = {
|
|
19
20
|
conversation: serverUrl + (config.convoPath || config.path || config_1.CONFIG.paths.conversation[this.chatApiV].base),
|
|
20
21
|
history: serverUrl + (config.historyPath || config_1.CONFIG.paths.history.chat.v1.base),
|
|
21
22
|
progressStream: serverUrl + (config.progressStreamPath || (progressConfig?.stream ?? config_1.CONFIG.paths.progress.v4.stream)),
|
|
22
23
|
progress: serverUrl + (config.progressPath || (progressConfig?.base ?? config_1.CONFIG.paths.progress.v4.base)),
|
|
24
|
+
stop: serverUrl + (interruptConfig?.stop ?? config_1.CONFIG.paths.interrupt.v4.stop),
|
|
25
|
+
steer: serverUrl + (interruptConfig?.steer ?? config_1.CONFIG.paths.interrupt.v4.steer),
|
|
23
26
|
};
|
|
24
27
|
this.apiKey = config.apiKey;
|
|
25
28
|
this.debug = config.debug || false;
|
|
@@ -36,6 +39,8 @@ class Conversation {
|
|
|
36
39
|
else {
|
|
37
40
|
this.transport = new transport_sse_1.SSEConversationTransport({
|
|
38
41
|
endpoint: () => this.endpoints.conversation,
|
|
42
|
+
stopEndpoint: () => this.endpoints.stop,
|
|
43
|
+
steerEndpoint: () => this.endpoints.steer,
|
|
39
44
|
apiKey: this.apiKey,
|
|
40
45
|
debug: this.debug,
|
|
41
46
|
});
|
|
@@ -261,6 +266,44 @@ class Conversation {
|
|
|
261
266
|
},
|
|
262
267
|
});
|
|
263
268
|
}
|
|
269
|
+
/**
|
|
270
|
+
* Stops the turn running in this conversation.
|
|
271
|
+
*
|
|
272
|
+
* `soft` (the default) lets the model finish the call it is in the middle of and takes no
|
|
273
|
+
* further step: the answer so far is kept, in history and in what the model remembers
|
|
274
|
+
* saying, and it works on every model. `hard` cuts the stream off mid-sentence and aborts
|
|
275
|
+
* the tools with it — but only actually stops the bill where the provider supports
|
|
276
|
+
* cancellation, so against one that does not the server applies a soft stop instead. The
|
|
277
|
+
* returned `mode` says which you got; `undefined` means there was nothing left to stop.
|
|
278
|
+
*
|
|
279
|
+
* Either way the turn's stream ends the way it always does, carrying what was produced
|
|
280
|
+
* before the stop. There is nothing else to clean up.
|
|
281
|
+
*/
|
|
282
|
+
async stop(mode = "soft") {
|
|
283
|
+
if (!this.convoId)
|
|
284
|
+
return undefined;
|
|
285
|
+
if (!this.transport.stop)
|
|
286
|
+
return undefined;
|
|
287
|
+
return this.transport.stop({ chatId: this.convoId, mode });
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Says something to the turn that is already running.
|
|
291
|
+
*
|
|
292
|
+
* The message reaches the model at its next step boundary, so nothing in flight is thrown
|
|
293
|
+
* away and the model reads it as the user talking mid-task — which is usually what someone
|
|
294
|
+
* typing while Alfred works actually means.
|
|
295
|
+
*
|
|
296
|
+
* Check the result. `too_late` means the message was NOT delivered, because the turn was
|
|
297
|
+
* already stopping or over, and it should be sent with `send()` instead. Dropping it is
|
|
298
|
+
* the one thing that is never right.
|
|
299
|
+
*/
|
|
300
|
+
async steer(message) {
|
|
301
|
+
if (!this.convoId)
|
|
302
|
+
return { ok: false, reason: "no_turn" };
|
|
303
|
+
if (!this.transport.steer)
|
|
304
|
+
return { ok: false, reason: "no_turn" };
|
|
305
|
+
return this.transport.steer({ chatId: this.convoId, message });
|
|
306
|
+
}
|
|
264
307
|
/**
|
|
265
308
|
* Sends a message and resolves with the finished reply.
|
|
266
309
|
*
|
|
@@ -6,16 +6,48 @@
|
|
|
6
6
|
* only decides how a turn is sent and how its stream comes back — everything a
|
|
7
7
|
* caller sees, including the payload shape, is identical either way.
|
|
8
8
|
*/
|
|
9
|
-
/** A turn in progress.
|
|
9
|
+
/** A turn in progress. */
|
|
10
10
|
export type ConversationStream = {
|
|
11
11
|
/**
|
|
12
12
|
* Stops listening. The turn itself continues server-side and its reply is still
|
|
13
|
-
* persisted, on both transports —
|
|
13
|
+
* persisted, on both transports — this is not a cancel. To actually stop the turn,
|
|
14
|
+
* use `Conversation.stop()`.
|
|
14
15
|
*/
|
|
15
16
|
close(): void;
|
|
16
17
|
/** The underlying EventSource, when the turn is being carried over SSE. */
|
|
17
18
|
readonly source?: unknown;
|
|
18
19
|
};
|
|
20
|
+
/**
|
|
21
|
+
* How hard to stop a turn.
|
|
22
|
+
*
|
|
23
|
+
* - `soft`: let the model call in flight finish, then stop. Keeps the text, costs what that
|
|
24
|
+
* step cost, and works everywhere. The right default for a stop button.
|
|
25
|
+
* - `hard`: abort the stream mid-sentence and every tool with it. Only stops the bill on
|
|
26
|
+
* providers that support cancellation; against one that does not the server applies a soft
|
|
27
|
+
* stop instead, rather than throwing away output that is billed either way. Check the
|
|
28
|
+
* `mode` you get back to see which happened.
|
|
29
|
+
*/
|
|
30
|
+
export type TurnStopMode = "soft" | "hard";
|
|
31
|
+
/** A stop, as the server applied it. */
|
|
32
|
+
export type TurnStopped = {
|
|
33
|
+
/** What was applied. */
|
|
34
|
+
mode: TurnStopMode;
|
|
35
|
+
/** What was asked for. Differs from `mode` when a hard stop was downgraded. */
|
|
36
|
+
requestedMode: TurnStopMode;
|
|
37
|
+
by: "user" | "system";
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* What became of a message steered into a running turn.
|
|
41
|
+
*
|
|
42
|
+
* `too_late` is the one that matters: the turn was already stopping or finished, the message
|
|
43
|
+
* was NOT delivered, and it should be sent as an ordinary turn instead. Never drop it.
|
|
44
|
+
*/
|
|
45
|
+
export type SteerResult = {
|
|
46
|
+
ok: true;
|
|
47
|
+
} | {
|
|
48
|
+
ok: false;
|
|
49
|
+
reason: "no_turn" | "too_late";
|
|
50
|
+
};
|
|
19
51
|
export type TransportTurnRequest = {
|
|
20
52
|
chatId?: string;
|
|
21
53
|
message: string;
|
|
@@ -36,8 +68,31 @@ export type TransportAttachRequest = {
|
|
|
36
68
|
/** Resume point: only what came after this event is replayed. */
|
|
37
69
|
afterEventId?: string;
|
|
38
70
|
};
|
|
71
|
+
/** A request to interrupt a turn that is already running. */
|
|
72
|
+
export type TransportStopRequest = {
|
|
73
|
+
chatId: string;
|
|
74
|
+
mode: TurnStopMode;
|
|
75
|
+
};
|
|
76
|
+
export type TransportSteerRequest = {
|
|
77
|
+
chatId: string;
|
|
78
|
+
message: string;
|
|
79
|
+
};
|
|
39
80
|
export interface ConversationTransport {
|
|
40
81
|
send(request: TransportTurnRequest, handlers: TransportHandlers): ConversationStream;
|
|
82
|
+
/**
|
|
83
|
+
* Stops the turn running in a conversation.
|
|
84
|
+
*
|
|
85
|
+
* Resolves `undefined` when there was nothing to stop — the turn finished on its own, or
|
|
86
|
+
* never started. That is not an error: it is the answer to pressing stop a moment too late.
|
|
87
|
+
*/
|
|
88
|
+
stop?(request: TransportStopRequest): Promise<TurnStopped | undefined>;
|
|
89
|
+
/**
|
|
90
|
+
* Says something to a turn that is still running, to be read at its next step boundary.
|
|
91
|
+
*
|
|
92
|
+
* The turn is not interrupted: work in flight finishes and the model reads the message as
|
|
93
|
+
* the user talking mid-task.
|
|
94
|
+
*/
|
|
95
|
+
steer?(request: TransportSteerRequest): Promise<SteerResult>;
|
|
41
96
|
/**
|
|
42
97
|
* Watches a turn that is already running, without starting one.
|
|
43
98
|
*
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Link } from "../link/link";
|
|
2
|
-
import { ConversationStream, ConversationTransport, TransportAttachRequest, TransportHandlers, TransportTurnRequest } from "./transport";
|
|
2
|
+
import { ConversationStream, ConversationTransport, SteerResult, TransportAttachRequest, TransportHandlers, TransportSteerRequest, TransportStopRequest, TransportTurnRequest, TurnStopped } from "./transport";
|
|
3
3
|
export type LinkSessionConfig = {
|
|
4
4
|
model?: string;
|
|
5
5
|
personality?: string;
|
|
@@ -20,6 +20,21 @@ export declare class LinkConversationTransport implements ConversationTransport
|
|
|
20
20
|
private sessionChatId?;
|
|
21
21
|
constructor(link: Link, config: () => LinkSessionConfig);
|
|
22
22
|
send(request: TransportTurnRequest, handlers: TransportHandlers): ConversationStream;
|
|
23
|
+
/**
|
|
24
|
+
* Stops a running turn.
|
|
25
|
+
*
|
|
26
|
+
* Addressed by conversation rather than by session, so a client that reconnected — losing
|
|
27
|
+
* its session but not the turn — can still stop what it is watching. The turn's own stream
|
|
28
|
+
* ends normally afterwards, carrying whatever the model produced before the stop.
|
|
29
|
+
*/
|
|
30
|
+
stop(request: TransportStopRequest): Promise<TurnStopped | undefined>;
|
|
31
|
+
/**
|
|
32
|
+
* Says something to a turn that is still running.
|
|
33
|
+
*
|
|
34
|
+
* A refusal is the interesting case: the caller is holding a message the user typed and
|
|
35
|
+
* has to send it as an ordinary turn instead of dropping it.
|
|
36
|
+
*/
|
|
37
|
+
steer(request: TransportSteerRequest): Promise<SteerResult>;
|
|
23
38
|
/** Ends the session, if one is open. The conversation can still be resumed later. */
|
|
24
39
|
end(): Promise<void>;
|
|
25
40
|
/**
|
|
@@ -33,6 +33,49 @@ class LinkConversationTransport {
|
|
|
33
33
|
});
|
|
34
34
|
return { close: () => { closed = true; } };
|
|
35
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Stops a running turn.
|
|
38
|
+
*
|
|
39
|
+
* Addressed by conversation rather than by session, so a client that reconnected — losing
|
|
40
|
+
* its session but not the turn — can still stop what it is watching. The turn's own stream
|
|
41
|
+
* ends normally afterwards, carrying whatever the model produced before the stop.
|
|
42
|
+
*/
|
|
43
|
+
async stop(request) {
|
|
44
|
+
try {
|
|
45
|
+
const frame = await this.link.exchange("conversation.stop", request, {
|
|
46
|
+
isDone: (reply) => reply.type === "conversation.stopped" || reply.type === "ack" || reply.type === "error",
|
|
47
|
+
});
|
|
48
|
+
if (frame.type !== "conversation.stopped")
|
|
49
|
+
return undefined;
|
|
50
|
+
const { mode, requestedMode, by } = frame.payload;
|
|
51
|
+
return { mode, requestedMode, by };
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// Nothing was running, or the link is gone. Either way there is no turn to stop and
|
|
55
|
+
// nothing useful for the caller to do about it.
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Says something to a turn that is still running.
|
|
61
|
+
*
|
|
62
|
+
* A refusal is the interesting case: the caller is holding a message the user typed and
|
|
63
|
+
* has to send it as an ordinary turn instead of dropping it.
|
|
64
|
+
*/
|
|
65
|
+
async steer(request) {
|
|
66
|
+
try {
|
|
67
|
+
const frame = await this.link.exchange("conversation.steer", request, {
|
|
68
|
+
isDone: (reply) => reply.type === "conversation.steered" || reply.type === "ack" || reply.type === "error",
|
|
69
|
+
});
|
|
70
|
+
if (frame.type === "conversation.steered")
|
|
71
|
+
return { ok: true };
|
|
72
|
+
return { ok: false, reason: "no_turn" };
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
const code = error instanceof protocol_1.LinkError ? error.code : undefined;
|
|
76
|
+
return { ok: false, reason: code === "too_late" ? "too_late" : "no_turn" };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
36
79
|
/** Ends the session, if one is open. The conversation can still be resumed later. */
|
|
37
80
|
async end() {
|
|
38
81
|
const sessionId = this.sessionId;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { EventSource } from "eventsource";
|
|
2
|
-
import { ConversationStream, ConversationTransport, TransportHandlers, TransportTurnRequest } from "./transport";
|
|
2
|
+
import { ConversationStream, ConversationTransport, SteerResult, TransportHandlers, TransportSteerRequest, TransportStopRequest, TransportTurnRequest, TurnStopped } from "./transport";
|
|
3
3
|
type StreamOptions = {
|
|
4
4
|
debug?: boolean;
|
|
5
5
|
onPayload(payload: {
|
|
@@ -20,9 +20,21 @@ export declare class SSEConversationTransport implements ConversationTransport {
|
|
|
20
20
|
private readonly config;
|
|
21
21
|
constructor(config: {
|
|
22
22
|
endpoint(): string;
|
|
23
|
+
stopEndpoint?(): string;
|
|
24
|
+
steerEndpoint?(): string;
|
|
23
25
|
apiKey: string;
|
|
24
26
|
debug?: boolean;
|
|
25
27
|
});
|
|
26
28
|
send(request: TransportTurnRequest, handlers: TransportHandlers): ConversationStream;
|
|
29
|
+
/**
|
|
30
|
+
* The turn is a GET that streams until it is done, so an interruption cannot travel on the
|
|
31
|
+
* same request and gets one of its own.
|
|
32
|
+
*
|
|
33
|
+
* Closing the stream is deliberately not a stop: a reload, a backgrounded tab or a
|
|
34
|
+
* reconnect all drop the connection and all expect to pick the answer back up.
|
|
35
|
+
*/
|
|
36
|
+
stop(request: TransportStopRequest): Promise<TurnStopped | undefined>;
|
|
37
|
+
steer(request: TransportSteerRequest): Promise<SteerResult>;
|
|
38
|
+
private post;
|
|
27
39
|
}
|
|
28
40
|
export {};
|
|
@@ -40,6 +40,68 @@ class SSEConversationTransport {
|
|
|
40
40
|
});
|
|
41
41
|
return { close: () => sse.close(), source: sse };
|
|
42
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* The turn is a GET that streams until it is done, so an interruption cannot travel on the
|
|
45
|
+
* same request and gets one of its own.
|
|
46
|
+
*
|
|
47
|
+
* Closing the stream is deliberately not a stop: a reload, a backgrounded tab or a
|
|
48
|
+
* reconnect all drop the connection and all expect to pick the answer back up.
|
|
49
|
+
*/
|
|
50
|
+
async stop(request) {
|
|
51
|
+
const endpoint = this.config.stopEndpoint?.();
|
|
52
|
+
if (!endpoint)
|
|
53
|
+
return undefined;
|
|
54
|
+
const response = await this.post(endpoint, { chatId: request.chatId, mode: request.mode });
|
|
55
|
+
if (!response)
|
|
56
|
+
return undefined;
|
|
57
|
+
// 404 is "nothing was running", which is the ordinary answer to stopping a moment late.
|
|
58
|
+
// Anything else is the server refusing or failing, and a stop button that quietly does
|
|
59
|
+
// nothing is the hardest kind of bug to report — so it is said out loud.
|
|
60
|
+
if (!response.ok) {
|
|
61
|
+
if (response.status !== 404)
|
|
62
|
+
console.warn(`[Stop failed: ${response.status}]`, await response.text().catch(() => ""));
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
const body = await response.json().catch(() => undefined);
|
|
66
|
+
return body?.stop;
|
|
67
|
+
}
|
|
68
|
+
async steer(request) {
|
|
69
|
+
const endpoint = this.config.steerEndpoint?.();
|
|
70
|
+
if (!endpoint)
|
|
71
|
+
return { ok: false, reason: "no_turn" };
|
|
72
|
+
const response = await this.post(endpoint, { chatId: request.chatId, message: request.message });
|
|
73
|
+
if (!response)
|
|
74
|
+
return { ok: false, reason: "no_turn" };
|
|
75
|
+
if (response.ok)
|
|
76
|
+
return { ok: true };
|
|
77
|
+
// 409: the turn is no longer accepting messages. The caller still has something the
|
|
78
|
+
// user typed and must send it as an ordinary turn.
|
|
79
|
+
if (response.status === 409)
|
|
80
|
+
return { ok: false, reason: "too_late" };
|
|
81
|
+
// Anything other than "no turn" means the message did not land for a reason the caller
|
|
82
|
+
// cannot see: it still falls back to sending, but not in silence.
|
|
83
|
+
if (response.status !== 404)
|
|
84
|
+
console.warn(`[Steer failed: ${response.status}]`, await response.text().catch(() => ""));
|
|
85
|
+
return { ok: false, reason: "no_turn" };
|
|
86
|
+
}
|
|
87
|
+
async post(endpoint, body) {
|
|
88
|
+
const url = (0, url_formatter_1.formatURL)(endpoint, undefined, { apiKey: this.config.apiKey, debug: this.config.debug });
|
|
89
|
+
try {
|
|
90
|
+
return await fetch(url, {
|
|
91
|
+
method: "POST",
|
|
92
|
+
headers: { "content-type": "application/json" },
|
|
93
|
+
body: JSON.stringify(body),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
// A stop that cannot reach the server is not worth throwing over: the turn is
|
|
98
|
+
// ending on its own soon enough, and the caller has no better move to make. It is
|
|
99
|
+
// still worth saying, and not only in debug — an interruption that vanishes without
|
|
100
|
+
// a trace is indistinguishable from one the UI forgot to wire up.
|
|
101
|
+
console.warn(`[Interrupt failed: ${endpoint}]`, error);
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
43
105
|
}
|
|
44
106
|
exports.SSEConversationTransport = SSEConversationTransport;
|
|
45
107
|
function asQuery(request) {
|
|
@@ -197,6 +197,16 @@ export type ConvoStatusPayload = {
|
|
|
197
197
|
};
|
|
198
198
|
export type ResponseStatusPayload = {
|
|
199
199
|
completed: boolean;
|
|
200
|
+
/** Present when the turn was stopped rather than finished on its own.
|
|
201
|
+
* `mode` is what was applied and `requestedMode` what was asked for: they differ when a
|
|
202
|
+
* hard stop was downgraded because the model's provider ignores a cancelled stream. */
|
|
203
|
+
stop?: {
|
|
204
|
+
mode: "soft" | "hard";
|
|
205
|
+
requestedMode: "soft" | "hard";
|
|
206
|
+
by: "user" | "system";
|
|
207
|
+
/** When the stop was requested. */
|
|
208
|
+
at: number;
|
|
209
|
+
};
|
|
200
210
|
/** Rich response metadata — only present on the final `completed: true` event.
|
|
201
211
|
* Populated by the caller (e.g. gateway) after the response finishes and usage is available. */
|
|
202
212
|
metadata?: ResponseMetadata;
|