setup-agent 0.0.3 → 0.0.5
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/LICENSE +21 -0
- package/README.md +11 -5
- package/dist/cli.d.ts +51 -46
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +81 -22
- package/dist/cli.js.map +1 -1
- package/dist/client.d.ts +3 -6
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +12 -4
- package/dist/client.js.map +1 -1
- package/dist/host.d.ts +7 -9
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +82 -75
- package/dist/host.js.map +1 -1
- package/dist/index.d.ts +7 -4
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -2
- package/dist/presets.d.ts +23 -25
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js +4 -3
- package/dist/presets.js.map +1 -1
- package/dist/protocol.d.ts +279 -262
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +35 -4
- package/dist/protocol.js.map +1 -1
- package/package.json +20 -13
- package/src/cli.test.ts +615 -0
- package/src/cli.ts +118 -29
- package/src/client.test.ts +54 -0
- package/src/client.ts +12 -4
- package/src/host.test.ts +1030 -0
- package/src/host.ts +52 -9
- package/src/index.ts +3 -0
- package/src/presets.ts +4 -3
- package/src/protocol.test.ts +128 -0
- package/src/protocol.ts +103 -5
package/src/host.ts
CHANGED
|
@@ -12,6 +12,9 @@ import {
|
|
|
12
12
|
isAgentPresent,
|
|
13
13
|
isClosed,
|
|
14
14
|
isValidModelAnswer,
|
|
15
|
+
isValidEntryPointAnswer,
|
|
16
|
+
isValidEntryPointInput,
|
|
17
|
+
parseEntryPointOptions,
|
|
15
18
|
parsePreviewUrl,
|
|
16
19
|
} from "./protocol";
|
|
17
20
|
import type { Checkout } from "./protocol";
|
|
@@ -83,6 +86,16 @@ const useCheckoutHost = (restored: unknown) => {
|
|
|
83
86
|
|
|
84
87
|
const ask = (seed: Checkout.InputSeed & { stepId?: string }) => {
|
|
85
88
|
const kind = seed.kind ?? "text";
|
|
89
|
+
const options =
|
|
90
|
+
kind === "entry-point"
|
|
91
|
+
? parseEntryPointOptions(seed.options)
|
|
92
|
+
: seed.options;
|
|
93
|
+
if (kind === "entry-point" && !isValidEntryPointInput(seed)) {
|
|
94
|
+
throw reject(
|
|
95
|
+
"invalid-input",
|
|
96
|
+
"an entry-point input needs a prompt and 1–3 distinct options with descriptions, form factors, placements and triggers; at most one is recommended",
|
|
97
|
+
);
|
|
98
|
+
}
|
|
86
99
|
if (kind === "product") {
|
|
87
100
|
if (!seed.product) {
|
|
88
101
|
throw reject("invalid-input", "a product input names a product");
|
|
@@ -98,13 +111,36 @@ const useCheckoutHost = (restored: unknown) => {
|
|
|
98
111
|
}
|
|
99
112
|
if (
|
|
100
113
|
seed.multiple &&
|
|
101
|
-
(kind !== "choice" ||
|
|
114
|
+
(kind !== "choice" ||
|
|
115
|
+
seed.options?.some((option) => option.variants?.length))
|
|
102
116
|
) {
|
|
103
117
|
throw reject(
|
|
104
118
|
"invalid-input",
|
|
105
119
|
"only a choice whose options have no variants accepts several answers",
|
|
106
120
|
);
|
|
107
121
|
}
|
|
122
|
+
const unique = (ids: readonly string[]) => new Set(ids).size === ids.length;
|
|
123
|
+
if (
|
|
124
|
+
!unique(seed.options?.map((option) => option.id) ?? []) ||
|
|
125
|
+
seed.options?.some(
|
|
126
|
+
(option) =>
|
|
127
|
+
!unique(option.variants?.map((variant) => variant.id) ?? []),
|
|
128
|
+
)
|
|
129
|
+
) {
|
|
130
|
+
throw reject("invalid-input", "option and variant ids must be unique");
|
|
131
|
+
}
|
|
132
|
+
if (
|
|
133
|
+
seed.options?.some(
|
|
134
|
+
(option) =>
|
|
135
|
+
option.id.includes(":") ||
|
|
136
|
+
option.variants?.some((variant) => variant.id.includes(":")),
|
|
137
|
+
)
|
|
138
|
+
) {
|
|
139
|
+
throw reject(
|
|
140
|
+
"invalid-input",
|
|
141
|
+
'option and variant ids cannot contain ":", which separates them in an answer',
|
|
142
|
+
);
|
|
143
|
+
}
|
|
108
144
|
if (
|
|
109
145
|
seed.default !== undefined &&
|
|
110
146
|
!seed.options?.some((option) => option.id === seed.default)
|
|
@@ -125,7 +161,7 @@ const useCheckoutHost = (restored: unknown) => {
|
|
|
125
161
|
...(seed.preset !== undefined && { preset: seed.preset }),
|
|
126
162
|
prompt: seed.prompt,
|
|
127
163
|
...(seed.placeholder !== undefined && { placeholder: seed.placeholder }),
|
|
128
|
-
...(
|
|
164
|
+
...(options !== undefined && { options }),
|
|
129
165
|
...(seed.multiple && { multiple: true }),
|
|
130
166
|
...(kind === "product" && { product: seed.product }),
|
|
131
167
|
...(seed.default !== undefined && { default: seed.default }),
|
|
@@ -214,6 +250,12 @@ const useCheckoutHost = (restored: unknown) => {
|
|
|
214
250
|
: `"${answer}" names an option without a valid variant`,
|
|
215
251
|
);
|
|
216
252
|
}
|
|
253
|
+
if (
|
|
254
|
+
input.kind === "entry-point" &&
|
|
255
|
+
!isValidEntryPointAnswer(input, answer)
|
|
256
|
+
) {
|
|
257
|
+
throw reject("invalid-answer", "an entry-point answer is an option id");
|
|
258
|
+
}
|
|
217
259
|
if (input.kind === "model" && !isValidModelAnswer(input, answer)) {
|
|
218
260
|
throw reject(
|
|
219
261
|
"invalid-answer",
|
|
@@ -305,8 +347,9 @@ const useCheckoutHost = (restored: unknown) => {
|
|
|
305
347
|
plan.decidedAt = Date.now();
|
|
306
348
|
plan.feedback = params.feedback.trim();
|
|
307
349
|
},
|
|
308
|
-
"checkout/cancel": () => {
|
|
309
|
-
if (state.
|
|
350
|
+
"checkout/cancel": ({ id } = {}) => {
|
|
351
|
+
if (state.createdAt === null || isClosed(state)) return;
|
|
352
|
+
if (id !== undefined && id !== state.id) return;
|
|
310
353
|
state.status = "cancelled";
|
|
311
354
|
dismissOpenInputs();
|
|
312
355
|
},
|
|
@@ -360,11 +403,11 @@ const useCheckoutHost = (restored: unknown) => {
|
|
|
360
403
|
},
|
|
361
404
|
"agent/add-step": ({ title, detail, product, active }) => {
|
|
362
405
|
requireInstalling();
|
|
363
|
-
if (
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
406
|
+
if (product !== undefined && product.trim() === "") {
|
|
407
|
+
throw reject(
|
|
408
|
+
"invalid-input",
|
|
409
|
+
"a step's product identifier cannot be empty",
|
|
410
|
+
);
|
|
368
411
|
}
|
|
369
412
|
const id = `s${state.steps.length + 1}`;
|
|
370
413
|
state.steps.push({
|
package/src/index.ts
CHANGED
package/src/presets.ts
CHANGED
|
@@ -10,7 +10,7 @@ export type PresetId = "framework" | "llm-provider" | "project";
|
|
|
10
10
|
export type PresetOverrides = {
|
|
11
11
|
/** Restrict the options to these ids, in this order; one id locks the choice in. */
|
|
12
12
|
only?: readonly string[];
|
|
13
|
-
/** Options the agent found itself, listed before the preset's own. */
|
|
13
|
+
/** Options the agent found itself, listed before the preset's own; `only` leaves them out. */
|
|
14
14
|
found?: readonly { id: string; description?: string }[];
|
|
15
15
|
default?: string;
|
|
16
16
|
optional?: boolean;
|
|
@@ -139,7 +139,8 @@ export const INPUT_PRESETS: Record<PresetId, Preset> = {
|
|
|
139
139
|
},
|
|
140
140
|
};
|
|
141
141
|
|
|
142
|
-
export const isPresetId = (id: string): id is PresetId =>
|
|
142
|
+
export const isPresetId = (id: string): id is PresetId =>
|
|
143
|
+
Object.hasOwn(INPUT_PRESETS, id);
|
|
143
144
|
|
|
144
145
|
/** Build the seed for a standard input, throwing on an unknown preset, option or default. */
|
|
145
146
|
export const presetInput = (
|
|
@@ -164,7 +165,7 @@ export const presetInput = (
|
|
|
164
165
|
}
|
|
165
166
|
return { ...option, label: option.id };
|
|
166
167
|
});
|
|
167
|
-
const options = [...found, ...listed];
|
|
168
|
+
const options = overrides.only ? listed : [...found, ...listed];
|
|
168
169
|
const fallback = options.some((o) => o.id === preset.default)
|
|
169
170
|
? preset.default
|
|
170
171
|
: undefined;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
isValidEntryPointAnswer,
|
|
4
|
+
isValidEntryPointInput,
|
|
5
|
+
parseEntryPointOptions,
|
|
6
|
+
type Checkout,
|
|
7
|
+
} from "./protocol";
|
|
8
|
+
|
|
9
|
+
const option: Checkout.EntryPointOption = {
|
|
10
|
+
id: "ticket-sidebar",
|
|
11
|
+
label: "Ticket sidebar",
|
|
12
|
+
description: "Draft replies beside the selected support ticket.",
|
|
13
|
+
entryPoint: {
|
|
14
|
+
formFactor: "sidebar",
|
|
15
|
+
placement: "The right side of the ticket detail page",
|
|
16
|
+
trigger: "Open with the Draft reply button in the ticket toolbar",
|
|
17
|
+
recommended: true,
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const input: Checkout.Input = {
|
|
22
|
+
id: "q1",
|
|
23
|
+
kind: "entry-point",
|
|
24
|
+
phase: "planning",
|
|
25
|
+
prompt: "Where should agents draft replies?",
|
|
26
|
+
options: [option],
|
|
27
|
+
default: option.id,
|
|
28
|
+
optional: false,
|
|
29
|
+
status: "open",
|
|
30
|
+
createdAt: 1,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
describe("entry-point options", () => {
|
|
34
|
+
it("keeps contextual metadata and permits different placements of the same form factor", () => {
|
|
35
|
+
const second = {
|
|
36
|
+
...option,
|
|
37
|
+
id: "inbox-sidebar",
|
|
38
|
+
entryPoint: {
|
|
39
|
+
...option.entryPoint,
|
|
40
|
+
placement: "The inbox list",
|
|
41
|
+
trigger: "Open with the inbox Assist button",
|
|
42
|
+
recommended: false,
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
expect(parseEntryPointOptions([option, second])).toEqual([option, second]);
|
|
46
|
+
expect(isValidEntryPointInput(input)).toBe(true);
|
|
47
|
+
expect(isValidEntryPointAnswer(input, "ticket-sidebar")).toBe(true);
|
|
48
|
+
expect(isValidEntryPointAnswer(input, "sidebar")).toBe(false);
|
|
49
|
+
expect(isValidEntryPointAnswer(input, "ticket-sidebar:variant")).toBe(
|
|
50
|
+
false,
|
|
51
|
+
);
|
|
52
|
+
expect(isValidEntryPointAnswer(input, '["ticket-sidebar"]')).toBe(false);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it.each(
|
|
56
|
+
[
|
|
57
|
+
null,
|
|
58
|
+
{},
|
|
59
|
+
[],
|
|
60
|
+
[null],
|
|
61
|
+
[option, option],
|
|
62
|
+
[option, { ...option, id: "another" }],
|
|
63
|
+
[
|
|
64
|
+
option,
|
|
65
|
+
{ ...option, id: "b" },
|
|
66
|
+
{ ...option, id: "c" },
|
|
67
|
+
{ ...option, id: "d" },
|
|
68
|
+
],
|
|
69
|
+
[{ ...option, id: "with:variant" }],
|
|
70
|
+
[{ ...option, id: "x".repeat(65) }],
|
|
71
|
+
[{ ...option, label: " " }],
|
|
72
|
+
[{ ...option, description: "" }],
|
|
73
|
+
[{ ...option, description: "x".repeat(1001) }],
|
|
74
|
+
[{ ...option, entryPoint: null }],
|
|
75
|
+
[
|
|
76
|
+
{
|
|
77
|
+
...option,
|
|
78
|
+
entryPoint: { ...option.entryPoint, formFactor: "drawer" },
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
[{ ...option, entryPoint: { ...option.entryPoint, placement: "" } }],
|
|
82
|
+
[{ ...option, entryPoint: { ...option.entryPoint, trigger: " " } }],
|
|
83
|
+
[{ ...option, entryPoint: { ...option.entryPoint, recommended: "yes" } }],
|
|
84
|
+
[{ ...option, variants: [] }],
|
|
85
|
+
].map((value) => ({ value })),
|
|
86
|
+
)("rejects malformed or ambiguous choices %#", ({ value }) => {
|
|
87
|
+
expect(parseEntryPointOptions(value)).toBeUndefined();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("types an option with only the fields the parser keeps", () => {
|
|
91
|
+
const withIcon: Checkout.EntryPointOption = {
|
|
92
|
+
...option,
|
|
93
|
+
// @ts-expect-error an entry-point option carries no icon
|
|
94
|
+
icon: "sidebar",
|
|
95
|
+
};
|
|
96
|
+
const withVariants: Checkout.EntryPointOption = {
|
|
97
|
+
...option,
|
|
98
|
+
// @ts-expect-error an entry-point option carries no variants
|
|
99
|
+
variants: [],
|
|
100
|
+
};
|
|
101
|
+
expect(parseEntryPointOptions([withIcon])).toEqual([option]);
|
|
102
|
+
expect(parseEntryPointOptions([withVariants])).toBeUndefined();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("strips undeclared payload fields", () => {
|
|
106
|
+
expect(
|
|
107
|
+
parseEntryPointOptions([{ ...option, icon: "untrusted", extra: 1 }]),
|
|
108
|
+
).toEqual([option]);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("requires an exact default and a single selection", () => {
|
|
112
|
+
expect(isValidEntryPointInput({ ...input, multiple: true })).toBe(false);
|
|
113
|
+
expect(isValidEntryPointInput({ ...input, default: "unknown" })).toBe(
|
|
114
|
+
false,
|
|
115
|
+
);
|
|
116
|
+
expect(isValidEntryPointInput({ ...input, prompt: " " })).toBe(false);
|
|
117
|
+
expect(isValidEntryPointInput({ ...input, kind: "choice" })).toBe(false);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("refuses a help note, which the entry-point card has no place for", () => {
|
|
121
|
+
expect(
|
|
122
|
+
isValidEntryPointInput({
|
|
123
|
+
...input,
|
|
124
|
+
help: { summary: "Pick one" },
|
|
125
|
+
}),
|
|
126
|
+
).toBe(false);
|
|
127
|
+
});
|
|
128
|
+
});
|
package/src/protocol.ts
CHANGED
|
@@ -32,7 +32,26 @@ export namespace Checkout {
|
|
|
32
32
|
* another product depends on. The browser owns the catalog, so it answers
|
|
33
33
|
* with `checkout/add-product`; dismissing the input declines.
|
|
34
34
|
*/
|
|
35
|
-
export type InputKind =
|
|
35
|
+
export type InputKind =
|
|
36
|
+
| "text"
|
|
37
|
+
| "choice"
|
|
38
|
+
| "model"
|
|
39
|
+
| "product"
|
|
40
|
+
| "entry-point";
|
|
41
|
+
|
|
42
|
+
export type EntryPoint = {
|
|
43
|
+
formFactor: "modal" | "sidebar" | "full-page";
|
|
44
|
+
placement: string;
|
|
45
|
+
trigger: string;
|
|
46
|
+
recommended?: boolean;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export type EntryPointOption = {
|
|
50
|
+
id: string;
|
|
51
|
+
label: string;
|
|
52
|
+
description: string;
|
|
53
|
+
entryPoint: EntryPoint;
|
|
54
|
+
};
|
|
36
55
|
|
|
37
56
|
export type ChoiceVariant = { id: string; label: string };
|
|
38
57
|
|
|
@@ -44,6 +63,7 @@ export namespace Checkout {
|
|
|
44
63
|
icon?: string;
|
|
45
64
|
/** A second pick under this option, such as the language of a framework. */
|
|
46
65
|
variants?: ChoiceVariant[];
|
|
66
|
+
entryPoint?: EntryPoint;
|
|
47
67
|
};
|
|
48
68
|
|
|
49
69
|
/** A short nudge for users who need help choosing, with a longer guide to link to. */
|
|
@@ -199,7 +219,8 @@ export namespace Checkout {
|
|
|
199
219
|
"agent/ack": (params: { messageId: string }) => void;
|
|
200
220
|
"checkout/dismiss": (params: { inputId: string }) => void;
|
|
201
221
|
"checkout/plan": (params: PlanDecision) => void;
|
|
202
|
-
|
|
222
|
+
/** A cancel that names another checkout than the current one is a no-op. */
|
|
223
|
+
"checkout/cancel": (params?: { id?: string }) => void;
|
|
203
224
|
"checkout/finish": () => void;
|
|
204
225
|
"agent/intro": (params: { kind?: string }) => void;
|
|
205
226
|
"agent/hello": (params: { cwd?: string; kind?: string }) => void;
|
|
@@ -282,12 +303,17 @@ export const isClosed = (state: Checkout.State) =>
|
|
|
282
303
|
export const finishProposed = (state: Checkout.State) =>
|
|
283
304
|
!isClosed(state) && state.completion !== undefined;
|
|
284
305
|
|
|
285
|
-
/** True
|
|
306
|
+
/** True while a message the user sent after the agent proposed to close still waits for the agent. */
|
|
286
307
|
export const followedUpSinceProposal = (state: Checkout.State) => {
|
|
287
308
|
const proposedAt = state.completion?.proposedAt;
|
|
288
309
|
return (
|
|
289
310
|
proposedAt !== undefined &&
|
|
290
|
-
state.log.some(
|
|
311
|
+
state.log.some(
|
|
312
|
+
(entry) =>
|
|
313
|
+
entry.role === "user" &&
|
|
314
|
+
entry.at > proposedAt &&
|
|
315
|
+
entry.acknowledgedAt === undefined,
|
|
316
|
+
)
|
|
291
317
|
);
|
|
292
318
|
};
|
|
293
319
|
|
|
@@ -376,7 +402,9 @@ export const isOptionIcon = (value: string): value is OptionIcon =>
|
|
|
376
402
|
(OPTION_ICONS as readonly string[]).includes(value);
|
|
377
403
|
|
|
378
404
|
export const parseChoiceAnswer = (answer: string) => {
|
|
379
|
-
const
|
|
405
|
+
const parts = answer.split(":");
|
|
406
|
+
if (parts.length > 2) return { option: answer };
|
|
407
|
+
const [option = "", variant] = parts;
|
|
380
408
|
return { option, ...(variant !== undefined && { variant }) };
|
|
381
409
|
};
|
|
382
410
|
|
|
@@ -430,6 +458,76 @@ export const classifyChoiceAnswer = (
|
|
|
430
458
|
return kinds.includes("custom") ? "custom" : "option";
|
|
431
459
|
};
|
|
432
460
|
|
|
461
|
+
const isEntryPointText = (value: unknown, max: number): value is string =>
|
|
462
|
+
typeof value === "string" && value.trim() !== "" && value.length <= max;
|
|
463
|
+
|
|
464
|
+
export const parseEntryPointOptions = (
|
|
465
|
+
value: unknown,
|
|
466
|
+
): Checkout.EntryPointOption[] | undefined => {
|
|
467
|
+
if (!Array.isArray(value) || value.length < 1 || value.length > 3)
|
|
468
|
+
return undefined;
|
|
469
|
+
const options: Checkout.EntryPointOption[] = [];
|
|
470
|
+
for (const candidate of value) {
|
|
471
|
+
if (typeof candidate !== "object" || candidate === null) return undefined;
|
|
472
|
+
const { id, label, description, entryPoint, variants } =
|
|
473
|
+
candidate as Record<string, unknown>;
|
|
474
|
+
if (
|
|
475
|
+
typeof id !== "string" ||
|
|
476
|
+
!/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$/.test(id) ||
|
|
477
|
+
options.some((option) => option.id === id) ||
|
|
478
|
+
!isEntryPointText(label, 160) ||
|
|
479
|
+
!isEntryPointText(description, 1000) ||
|
|
480
|
+
variants !== undefined ||
|
|
481
|
+
typeof entryPoint !== "object" ||
|
|
482
|
+
entryPoint === null
|
|
483
|
+
)
|
|
484
|
+
return undefined;
|
|
485
|
+
const { formFactor, placement, trigger, recommended } =
|
|
486
|
+
entryPoint as Record<string, unknown>;
|
|
487
|
+
if (
|
|
488
|
+
(formFactor !== "modal" &&
|
|
489
|
+
formFactor !== "sidebar" &&
|
|
490
|
+
formFactor !== "full-page") ||
|
|
491
|
+
!isEntryPointText(placement, 400) ||
|
|
492
|
+
!isEntryPointText(trigger, 400) ||
|
|
493
|
+
(recommended !== undefined && typeof recommended !== "boolean")
|
|
494
|
+
)
|
|
495
|
+
return undefined;
|
|
496
|
+
options.push({
|
|
497
|
+
id,
|
|
498
|
+
label,
|
|
499
|
+
description,
|
|
500
|
+
entryPoint: {
|
|
501
|
+
formFactor,
|
|
502
|
+
placement,
|
|
503
|
+
trigger,
|
|
504
|
+
...(recommended !== undefined && { recommended }),
|
|
505
|
+
},
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
return options.filter((option) => option.entryPoint.recommended).length <= 1
|
|
509
|
+
? options
|
|
510
|
+
: undefined;
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
export const isValidEntryPointInput = (input: Checkout.InputSeed) => {
|
|
514
|
+
const options = parseEntryPointOptions(input.options);
|
|
515
|
+
return (
|
|
516
|
+
input.kind === "entry-point" &&
|
|
517
|
+
isEntryPointText(input.prompt, 1000) &&
|
|
518
|
+
options !== undefined &&
|
|
519
|
+
input.multiple === undefined &&
|
|
520
|
+
input.help === undefined &&
|
|
521
|
+
(input.default === undefined ||
|
|
522
|
+
options.some((option) => option.id === input.default))
|
|
523
|
+
);
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
export const isValidEntryPointAnswer = (
|
|
527
|
+
input: Checkout.Input,
|
|
528
|
+
answer: string,
|
|
529
|
+
) => input.options?.some((option) => option.id === answer) ?? false;
|
|
530
|
+
|
|
433
531
|
const REASONING_EFFORTS = new Set(["low", "medium", "high"]);
|
|
434
532
|
|
|
435
533
|
const isReasoningEffort = (
|