@frockbot/client-core 0.3.11 → 0.3.12
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/package.json +5 -5
- package/src/errors.test.ts +142 -0
- package/src/errors.ts +242 -0
- package/src/index.ts +26 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frockbot/client-core",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.12",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -13,10 +13,10 @@
|
|
|
13
13
|
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@frockbot/configuration-core": "0.3.
|
|
17
|
-
"@frockbot/connection-core": "0.3.
|
|
18
|
-
"@frockbot/kernel-contracts": "0.3.
|
|
19
|
-
"@frockbot/protocol": "0.3.
|
|
16
|
+
"@frockbot/configuration-core": "0.3.12",
|
|
17
|
+
"@frockbot/connection-core": "0.3.12",
|
|
18
|
+
"@frockbot/kernel-contracts": "0.3.12",
|
|
19
|
+
"@frockbot/protocol": "0.3.12",
|
|
20
20
|
"vue": "3.5.41"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
classifyClientFailureV1,
|
|
4
|
+
clientFailureDetailV1,
|
|
5
|
+
presentClientFailureV1,
|
|
6
|
+
readJsonResponseV1,
|
|
7
|
+
serverRefusalMessageV1,
|
|
8
|
+
TransportFailureV1,
|
|
9
|
+
} from "./errors.js";
|
|
10
|
+
|
|
11
|
+
describe("readJsonResponseV1", () => {
|
|
12
|
+
test("returns the parsed body of a good response", async () => {
|
|
13
|
+
const response = new Response(JSON.stringify({ ok: true }), {
|
|
14
|
+
status: 200,
|
|
15
|
+
});
|
|
16
|
+
expect(await readJsonResponseV1(response)).toEqual({ ok: true });
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("an HTML error body is a transport failure, not a parse error", async () => {
|
|
20
|
+
// Incident 1: the visible symptom was `Unexpected token '<'` in the
|
|
21
|
+
// sidebar.
|
|
22
|
+
const response = new Response("<html><body>Bad gateway</body></html>", {
|
|
23
|
+
status: 502,
|
|
24
|
+
headers: { "content-type": "text/html" },
|
|
25
|
+
});
|
|
26
|
+
const failure = await readJsonResponseV1(response).catch(
|
|
27
|
+
(error: unknown) => error,
|
|
28
|
+
);
|
|
29
|
+
expect(failure).toBeInstanceOf(TransportFailureV1);
|
|
30
|
+
const transport = failure as TransportFailureV1;
|
|
31
|
+
expect(transport.kind).toBe("unreachable");
|
|
32
|
+
expect(transport.message).not.toContain("JSON");
|
|
33
|
+
expect(transport.message).toBe("Couldn't reach FrockBot.");
|
|
34
|
+
expect(transport.detail).toContain("Bad gateway");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("a 200 with a non-JSON body is unreachable rather than a crash", async () => {
|
|
38
|
+
const response = new Response("<html>proxy</html>", { status: 200 });
|
|
39
|
+
const failure = (await readJsonResponseV1(response).catch(
|
|
40
|
+
(error: unknown) => error,
|
|
41
|
+
)) as TransportFailureV1;
|
|
42
|
+
expect(failure.kind).toBe("unreachable");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("a refusal's own sentence is offered, a fault's is not", async () => {
|
|
46
|
+
// A 4xx is the deployment explaining a rule it holds, in words it wrote
|
|
47
|
+
// for a person; a 5xx is a fault, and its text is about plumbing.
|
|
48
|
+
const refusal = (await readJsonResponseV1(
|
|
49
|
+
new Response(
|
|
50
|
+
JSON.stringify({ error: "Your message is too long. Keep it short." }),
|
|
51
|
+
{ status: 413 },
|
|
52
|
+
),
|
|
53
|
+
).catch((error: unknown) => error)) as TransportFailureV1;
|
|
54
|
+
expect(serverRefusalMessageV1(refusal)).toBe(
|
|
55
|
+
"Your message is too long. Keep it short.",
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
const fault = (await readJsonResponseV1(
|
|
59
|
+
new Response(JSON.stringify({ error: "R2 is down" }), { status: 500 }),
|
|
60
|
+
).catch((error: unknown) => error)) as TransportFailureV1;
|
|
61
|
+
expect(serverRefusalMessageV1(fault)).toBeUndefined();
|
|
62
|
+
expect(fault.detail).toBe("R2 is down");
|
|
63
|
+
|
|
64
|
+
// Nothing written, nothing to offer.
|
|
65
|
+
const bare = (await readJsonResponseV1(
|
|
66
|
+
new Response("{}", { status: 400 }),
|
|
67
|
+
).catch((error: unknown) => error)) as TransportFailureV1;
|
|
68
|
+
expect(serverRefusalMessageV1(bare)).toBeUndefined();
|
|
69
|
+
expect(serverRefusalMessageV1(new Error("boom"))).toBeUndefined();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("the server's error field is kept as detail, not as the message", async () => {
|
|
73
|
+
const response = new Response(JSON.stringify({ error: "boom" }), {
|
|
74
|
+
status: 500,
|
|
75
|
+
});
|
|
76
|
+
const failure = (await readJsonResponseV1(response).catch(
|
|
77
|
+
(error: unknown) => error,
|
|
78
|
+
)) as TransportFailureV1;
|
|
79
|
+
expect(failure.kind).toBe("server");
|
|
80
|
+
expect(failure.detail).toBe("boom");
|
|
81
|
+
expect(failure.message).not.toBe("boom");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("carries the definitive flag through", async () => {
|
|
85
|
+
const response = new Response(
|
|
86
|
+
JSON.stringify({ error: "no", definitive: true }),
|
|
87
|
+
{ status: 400 },
|
|
88
|
+
);
|
|
89
|
+
const failure = (await readJsonResponseV1(response).catch(
|
|
90
|
+
(error: unknown) => error,
|
|
91
|
+
)) as TransportFailureV1;
|
|
92
|
+
expect(failure.definitive).toBe(true);
|
|
93
|
+
expect(failure.kind).toBe("rejected");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("classifies by status", async () => {
|
|
97
|
+
const kindOf = async (status: number): Promise<string> => {
|
|
98
|
+
const failure = (await readJsonResponseV1(
|
|
99
|
+
new Response("{}", { status }),
|
|
100
|
+
).catch((error: unknown) => error)) as TransportFailureV1;
|
|
101
|
+
return failure.kind;
|
|
102
|
+
};
|
|
103
|
+
expect(await kindOf(401)).toBe("denied");
|
|
104
|
+
expect(await kindOf(403)).toBe("denied");
|
|
105
|
+
expect(await kindOf(404)).toBe("missing");
|
|
106
|
+
expect(await kindOf(500)).toBe("server");
|
|
107
|
+
expect(await kindOf(503)).toBe("unreachable");
|
|
108
|
+
expect(await kindOf(422)).toBe("rejected");
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe("presentClientFailureV1", () => {
|
|
113
|
+
test("names what the User was doing", () => {
|
|
114
|
+
const failure = new TransportFailureV1({
|
|
115
|
+
kind: "server",
|
|
116
|
+
detail: "boom",
|
|
117
|
+
});
|
|
118
|
+
expect(presentClientFailureV1(failure, "load your plugins")).toBe(
|
|
119
|
+
"Couldn't load your plugins. Something went wrong at our end.",
|
|
120
|
+
);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("never repeats a parse error", () => {
|
|
124
|
+
const parse = new SyntaxError(
|
|
125
|
+
`Unexpected token '<', "<html><bod"... is not valid JSON`,
|
|
126
|
+
);
|
|
127
|
+
const sentence = presentClientFailureV1(parse, "load your flock");
|
|
128
|
+
expect(sentence).not.toContain("JSON");
|
|
129
|
+
expect(sentence).toBe("Couldn't load your flock — FrockBot didn't answer.");
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("a dropped fetch reads as unreachable", () => {
|
|
133
|
+
expect(presentClientFailureV1(new TypeError("Failed to fetch"))).toBe(
|
|
134
|
+
"Couldn't reach FrockBot.",
|
|
135
|
+
);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("keeps the raw text available for the console", () => {
|
|
139
|
+
expect(clientFailureDetailV1(new Error("boom"))).toBe("boom");
|
|
140
|
+
expect(classifyClientFailureV1("boom").detail).toBe("boom");
|
|
141
|
+
});
|
|
142
|
+
});
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The client's one boundary between a failed request and a sentence a person
|
|
3
|
+
* reads.
|
|
4
|
+
*
|
|
5
|
+
* Two habits produced the failures this module exists to stop. The first was
|
|
6
|
+
* calling `response.json()` before looking at the response: a 502 whose body is
|
|
7
|
+
* an HTML error page then fails inside `JSON.parse`, and what reached the
|
|
8
|
+
* screen was `Unexpected token '<'` — a message about this client's own
|
|
9
|
+
* parsing, in a place the User was told about their Bots. The second was
|
|
10
|
+
* putting the server's `error` field on screen verbatim: a 500 whose body was
|
|
11
|
+
* `{"error":"boom"}` rendered as the single red word `boom`.
|
|
12
|
+
*
|
|
13
|
+
* So: `readJsonResponseV1` is the only way a response becomes a value, and it
|
|
14
|
+
* throws `TransportFailureV1` with the raw text kept out of the message;
|
|
15
|
+
* `presentClientFailureV1` is the only way a caught error becomes text on a
|
|
16
|
+
* surface. Callers pass what the User was trying to do — "load your plugins" —
|
|
17
|
+
* and get one short sentence back. The raw detail stays on the error for the
|
|
18
|
+
* console.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/** What went wrong, at the coarseness a sentence can be written from. */
|
|
22
|
+
export type ClientFailureKindV1 =
|
|
23
|
+
"offline" | "unreachable" | "server" | "denied" | "missing" | "rejected";
|
|
24
|
+
|
|
25
|
+
export interface ClientFailureV1 {
|
|
26
|
+
readonly kind: ClientFailureKindV1;
|
|
27
|
+
readonly status?: number;
|
|
28
|
+
/** The raw text — for the console and for tests, never for the screen. */
|
|
29
|
+
readonly detail: string;
|
|
30
|
+
/**
|
|
31
|
+
* The sentence the deployment wrote for a person, when it wrote one: the
|
|
32
|
+
* `error` field of a refusal it decided on, like "Your message is too long."
|
|
33
|
+
* A refusal is the product explaining a rule it holds, so that sentence is
|
|
34
|
+
* the User's to read — unlike a fault's text, which is about plumbing. Only
|
|
35
|
+
* set for a 4xx, and only when the body actually carried one.
|
|
36
|
+
*/
|
|
37
|
+
readonly serverMessage?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** A request that did not produce a usable JSON body. */
|
|
41
|
+
export class TransportFailureV1 extends Error implements ClientFailureV1 {
|
|
42
|
+
readonly kind: ClientFailureKindV1;
|
|
43
|
+
readonly status?: number;
|
|
44
|
+
readonly detail: string;
|
|
45
|
+
readonly serverMessage?: string;
|
|
46
|
+
/**
|
|
47
|
+
* The server said this outcome is final, so a caller that retries pending
|
|
48
|
+
* work must stop rather than try again. Carried through because the flock's
|
|
49
|
+
* pending-create replay depends on it.
|
|
50
|
+
*/
|
|
51
|
+
readonly definitive?: boolean;
|
|
52
|
+
|
|
53
|
+
constructor(failure: ClientFailureV1 & { definitive?: boolean }) {
|
|
54
|
+
// The message is the presentable sentence, so an error that escapes to a
|
|
55
|
+
// `catch` that only reads `.message` still says something sane.
|
|
56
|
+
super(presentClientFailureV1Kind(failure.kind));
|
|
57
|
+
this.name = "TransportFailureV1";
|
|
58
|
+
this.kind = failure.kind;
|
|
59
|
+
if (failure.status !== undefined) this.status = failure.status;
|
|
60
|
+
this.detail = failure.detail;
|
|
61
|
+
if (failure.serverMessage !== undefined) {
|
|
62
|
+
this.serverMessage = failure.serverMessage;
|
|
63
|
+
}
|
|
64
|
+
if (failure.definitive !== undefined) this.definitive = failure.definitive;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function kindForStatusV1(status: number): ClientFailureKindV1 {
|
|
69
|
+
if (status === 401 || status === 403) return "denied";
|
|
70
|
+
if (status === 404 || status === 410) return "missing";
|
|
71
|
+
if (status === 408 || status === 429) return "unreachable";
|
|
72
|
+
if (status === 502 || status === 503 || status === 504) return "unreachable";
|
|
73
|
+
if (status >= 500) return "server";
|
|
74
|
+
return "rejected";
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function serverErrorStringV1(value: unknown): string | undefined {
|
|
78
|
+
if (
|
|
79
|
+
typeof value === "object" &&
|
|
80
|
+
value !== null &&
|
|
81
|
+
"error" in value &&
|
|
82
|
+
typeof (value as { error: unknown }).error === "string"
|
|
83
|
+
) {
|
|
84
|
+
const written = (value as { error: string }).error.trim();
|
|
85
|
+
return written === "" ? undefined : written;
|
|
86
|
+
}
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function isDefinitiveV1(value: unknown): boolean {
|
|
91
|
+
return (
|
|
92
|
+
typeof value === "object" &&
|
|
93
|
+
value !== null &&
|
|
94
|
+
"definitive" in value &&
|
|
95
|
+
(value as { definitive: unknown }).definitive === true
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Read a response as JSON, or throw a failure that can be presented.
|
|
101
|
+
*
|
|
102
|
+
* The body is taken as text first so a non-JSON body is a *classified*
|
|
103
|
+
* failure rather than a `SyntaxError` about this client's parser.
|
|
104
|
+
*/
|
|
105
|
+
export async function readJsonResponseV1(response: Response): Promise<unknown> {
|
|
106
|
+
const body = await response.text().catch(() => "");
|
|
107
|
+
let parsed: unknown;
|
|
108
|
+
let readable = true;
|
|
109
|
+
try {
|
|
110
|
+
parsed = body.trim() === "" ? undefined : (JSON.parse(body) as unknown);
|
|
111
|
+
} catch {
|
|
112
|
+
readable = false;
|
|
113
|
+
}
|
|
114
|
+
if (!response.ok) {
|
|
115
|
+
const written = readable ? serverErrorStringV1(parsed) : undefined;
|
|
116
|
+
throw new TransportFailureV1({
|
|
117
|
+
kind: kindForStatusV1(response.status),
|
|
118
|
+
status: response.status,
|
|
119
|
+
detail:
|
|
120
|
+
written ??
|
|
121
|
+
(readable
|
|
122
|
+
? `HTTP ${response.status}`
|
|
123
|
+
: `HTTP ${response.status}: ${body.slice(0, 200)}`),
|
|
124
|
+
// A refusal the deployment decided on, in words it chose for a person.
|
|
125
|
+
// A fault's text is never that, whatever it happens to say.
|
|
126
|
+
...(written !== undefined && response.status < 500
|
|
127
|
+
? { serverMessage: written }
|
|
128
|
+
: {}),
|
|
129
|
+
...(readable && isDefinitiveV1(parsed) ? { definitive: true } : {}),
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
if (!readable) {
|
|
133
|
+
// A 200 that is not JSON is something between this client and the
|
|
134
|
+
// deployment answering — a proxy page, a captive portal, a stale worker.
|
|
135
|
+
throw new TransportFailureV1({
|
|
136
|
+
kind: "unreachable",
|
|
137
|
+
status: response.status,
|
|
138
|
+
detail: `Response body was not JSON: ${body.slice(0, 200)}`,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
return parsed;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** What a caught value amounts to, whatever threw it. */
|
|
145
|
+
export function classifyClientFailureV1(error: unknown): ClientFailureV1 {
|
|
146
|
+
if (error instanceof TransportFailureV1) return error;
|
|
147
|
+
const detail =
|
|
148
|
+
error instanceof Error ? error.message : String(error ?? "unknown error");
|
|
149
|
+
/*
|
|
150
|
+
* A transport that is not this one may still have read a status and put it
|
|
151
|
+
* on the error — the desktop bridge does, and so does anything that decodes
|
|
152
|
+
* a refusal before throwing. A 4xx there means the same thing it means here:
|
|
153
|
+
* the deployment read the request, refused it, and said why in words meant
|
|
154
|
+
* for a person.
|
|
155
|
+
*/
|
|
156
|
+
const status =
|
|
157
|
+
typeof error === "object" &&
|
|
158
|
+
error !== null &&
|
|
159
|
+
"status" in error &&
|
|
160
|
+
typeof (error as { status: unknown }).status === "number"
|
|
161
|
+
? (error as { status: number }).status
|
|
162
|
+
: undefined;
|
|
163
|
+
if (status !== undefined && status >= 400) {
|
|
164
|
+
return {
|
|
165
|
+
kind: kindForStatusV1(status),
|
|
166
|
+
status,
|
|
167
|
+
detail,
|
|
168
|
+
...(status < 500 && detail !== "" ? { serverMessage: detail } : {}),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
// `fetch` rejects with a TypeError when the request never left, or never
|
|
172
|
+
// came back. The browser's own offline flag is the only way to tell the two
|
|
173
|
+
// apart, and it is advisory, so it only picks the wording.
|
|
174
|
+
const offline =
|
|
175
|
+
typeof navigator !== "undefined" && navigator.onLine === false;
|
|
176
|
+
if (error instanceof TypeError) {
|
|
177
|
+
return { kind: offline ? "offline" : "unreachable", detail };
|
|
178
|
+
}
|
|
179
|
+
if (/JSON|Unexpected token/i.test(detail)) {
|
|
180
|
+
return { kind: "unreachable", detail };
|
|
181
|
+
}
|
|
182
|
+
return { kind: offline ? "offline" : "rejected", detail };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function presentClientFailureV1Kind(
|
|
186
|
+
kind: ClientFailureKindV1,
|
|
187
|
+
action?: string,
|
|
188
|
+
): string {
|
|
189
|
+
const doing = action ? ` ${action}` : "";
|
|
190
|
+
switch (kind) {
|
|
191
|
+
case "offline":
|
|
192
|
+
return "You're offline. Reconnect and try again.";
|
|
193
|
+
case "unreachable":
|
|
194
|
+
return action
|
|
195
|
+
? `Couldn't${doing} — FrockBot didn't answer.`
|
|
196
|
+
: "Couldn't reach FrockBot.";
|
|
197
|
+
case "server":
|
|
198
|
+
return action
|
|
199
|
+
? `Couldn't${doing}. Something went wrong at our end.`
|
|
200
|
+
: "Something went wrong at our end.";
|
|
201
|
+
case "denied":
|
|
202
|
+
return "You're not signed in any more. Sign in again.";
|
|
203
|
+
case "missing":
|
|
204
|
+
return action
|
|
205
|
+
? `Couldn't${doing} — it isn't there.`
|
|
206
|
+
: "That isn't there.";
|
|
207
|
+
case "rejected":
|
|
208
|
+
return action ? `Couldn't${doing}.` : "That didn't work.";
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* One short sentence for a failed request.
|
|
214
|
+
*
|
|
215
|
+
* `action` is what the User was doing, as a verb phrase that reads after
|
|
216
|
+
* "Couldn't": `presentClientFailureV1(error, "load your plugins")`.
|
|
217
|
+
*/
|
|
218
|
+
export function presentClientFailureV1(
|
|
219
|
+
error: unknown,
|
|
220
|
+
action?: string,
|
|
221
|
+
): string {
|
|
222
|
+
return presentClientFailureV1Kind(
|
|
223
|
+
classifyClientFailureV1(error).kind,
|
|
224
|
+
action,
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* The sentence the deployment wrote for a person, if it wrote one.
|
|
230
|
+
*
|
|
231
|
+
* Use it where a refusal's own reason is what the User needs — the send that
|
|
232
|
+
* was too long, the plugin that needs another turned on first — and fall back
|
|
233
|
+
* to `presentClientFailureV1`. A fault never carries one.
|
|
234
|
+
*/
|
|
235
|
+
export function serverRefusalMessageV1(error: unknown): string | undefined {
|
|
236
|
+
return classifyClientFailureV1(error).serverMessage;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** The raw text, for a console line or a log — never for a surface. */
|
|
240
|
+
export function clientFailureDetailV1(error: unknown): string {
|
|
241
|
+
return classifyClientFailureV1(error).detail;
|
|
242
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -12,6 +12,17 @@ import { decodeExternalAuthorizationUrl } from "@frockbot/protocol";
|
|
|
12
12
|
|
|
13
13
|
export { decodeExternalAuthorizationUrl };
|
|
14
14
|
|
|
15
|
+
export {
|
|
16
|
+
classifyClientFailureV1,
|
|
17
|
+
clientFailureDetailV1,
|
|
18
|
+
presentClientFailureV1,
|
|
19
|
+
readJsonResponseV1,
|
|
20
|
+
serverRefusalMessageV1,
|
|
21
|
+
TransportFailureV1,
|
|
22
|
+
type ClientFailureKindV1,
|
|
23
|
+
type ClientFailureV1,
|
|
24
|
+
} from "./errors.js";
|
|
25
|
+
|
|
15
26
|
import type {
|
|
16
27
|
ConfigurationCommandV1,
|
|
17
28
|
ConfigurationQueryV1,
|
|
@@ -103,14 +114,21 @@ export type ClientStartConnectionResult = StartConnectionResult;
|
|
|
103
114
|
* A durable Session event that belongs to no Turn — a rename, today. The chat
|
|
104
115
|
* shows it as a system line rather than as a message from either party.
|
|
105
116
|
*/
|
|
106
|
-
export
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
117
|
+
export type ClientAnnouncement =
|
|
118
|
+
| {
|
|
119
|
+
type: "bot/renamed";
|
|
120
|
+
announcementId: string;
|
|
121
|
+
at: string;
|
|
122
|
+
from: string;
|
|
123
|
+
to: string;
|
|
124
|
+
namedBy: "user" | "bot";
|
|
125
|
+
}
|
|
126
|
+
| {
|
|
127
|
+
type: "conversation/compacted";
|
|
128
|
+
announcementId: string;
|
|
129
|
+
at: string;
|
|
130
|
+
throughTurn: number;
|
|
131
|
+
};
|
|
114
132
|
|
|
115
133
|
export interface ClientRun {
|
|
116
134
|
runId: string;
|