import type { ProviderUserInputAnswers, UserInputQuestion } from "@t3tools/contracts";import * as Schema from "effect/Schema"; const XAiAskUserQuestionOption = Schema.Struct({ label: Schema.String, description: Schema.optional(Schema.String), preview: Schema.optional(Schema.String), id: Schema.optional(Schema.String),}); const XAiAskUserQuestion = Schema.Struct({ id: Schema.optional(Schema.String), question: Schema.String, options: Schema.Array(XAiAskUserQuestionOption), multiSelect: Schema.optional(Schema.NullOr(Schema.Boolean)),}); const XAiAskUserQuestionParams = Schema.Struct({ sessionId: Schema.String, toolCallId: Schema.String, questions: Schema.Array(XAiAskUserQuestion), mode: Schema.Literals(["default", "plan"]),}); const XAiWrappedAskUserQuestionParams = Schema.Struct({ method: Schema.Literals(["x.ai/ask_user_question", "_x.ai/ask_user_question"]), params: XAiAskUserQuestionParams,}); export const XAiAskUserQuestionRequest = Schema.Union([ XAiAskUserQuestionParams, XAiWrappedAskUserQuestionParams,]); type XAiAskUserQuestionRequestParams = typeof XAiAskUserQuestionParams.Type;type XAiAskUserQuestionRequest = typeof XAiAskUserQuestionRequest.Type; function trimmed(value: string | undefined): string | undefined { const text = value?.trim(); return text && text.length > 0 ? text : undefined;} function unwrapAskUserQuestionParams( params: XAiAskUserQuestionRequest,): XAiAskUserQuestionRequestParams { return "params" in params ? params.params : params;} export function extractXAiAskUserQuestions( params: XAiAskUserQuestionRequest,): ReadonlyArray<UserInputQuestion> { return unwrapAskUserQuestionParams(params).questions.map((question) => ({ id: question.id ?? question.question, header: "Question", question: question.question, multiSelect: question.multiSelect === true, options: question.options.length > 0 ? question.options.map((option) => ({ label: option.label, description: option.description ?? option.label, })) : [{ label: "OK", description: "Continue" }], }));} interface XAiAskUserQuestionAnnotation { readonly preview?: string; readonly notes?: string;} interface XAiAskUserQuestionAcceptedResponse { readonly outcome: "accepted"; readonly answers: Record<string, ReadonlyArray<string>>; readonly annotations?: Record<string, XAiAskUserQuestionAnnotation>;} interface XAiAskUserQuestionCancelledResponse { readonly outcome: "cancelled";} export type XAiAskUserQuestionResponse = | XAiAskUserQuestionAcceptedResponse | XAiAskUserQuestionCancelledResponse; interface NormalizedXAiAnswer { readonly questionText: string; readonly selectedLabels: ReadonlyArray<string>; readonly annotation?: XAiAskUserQuestionAnnotation;} function answerValues(answer: unknown): ReadonlyArray<string> { if (Array.isArray(answer)) { return answer.flatMap((entry) => { const text = typeof entry === "string" ? trimmed(entry) : undefined; return text ? [text] : []; }); } const text = typeof answer === "string" ? trimmed(answer) : undefined; return text ? [text] : [];} function normalizeAnswerForXAi( question: XAiAskUserQuestionRequestParams["questions"][number], answer: unknown,): NormalizedXAiAnswer | undefined { const values = answerValues(answer); if (values.length === 0) { return undefined; } const optionByLabel = new Map(question.options.map((option) => [option.label, option])); const resolvedValues = values.map((value) => ({ value, option: optionByLabel.get(value), })); const selectedLabels = resolvedValues.flatMap(({ option }) => (option ? [option.label] : [])); const notes = resolvedValues.flatMap(({ option, value }) => (option ? [] : [value])); const preview = question.multiSelect === true ? undefined : resolvedValues.map(({ option }) => trimmed(option?.preview)).find((value) => value); const annotation = preview || notes.length > 0 ? { ...(preview ? { preview } : {}), ...(notes.length > 0 ? { notes: notes.join("\n") } : {}), } : undefined; return { questionText: question.question, selectedLabels: selectedLabels.length > 0 ? selectedLabels : ["Other"], ...(annotation ? { annotation } : {}), };} function findQuestionAnswer( answers: ProviderUserInputAnswers, question: XAiAskUserQuestionRequestParams["questions"][number],): unknown { const key = question.id ?? question.question; return answers[key] ?? answers[question.question];} export function makeXAiAskUserQuestionResponse( params: XAiAskUserQuestionRequest, answers: ProviderUserInputAnswers,): XAiAskUserQuestionAcceptedResponse { const questions = unwrapAskUserQuestionParams(params).questions; const normalized = questions.flatMap((question) => { const entry = normalizeAnswerForXAi(question, findQuestionAnswer(answers, question)); return entry ? [entry] : []; }); const annotations = Object.fromEntries( normalized.flatMap((entry) => entry.annotation ? [[entry.questionText, entry.annotation] as const] : [], ), ); return { outcome: "accepted", answers: Object.fromEntries( normalized.map((entry) => [entry.questionText, entry.selectedLabels]), ), ...(Object.keys(annotations).length > 0 ? { annotations } : {}), };} export function makeXAiAskUserQuestionCancelledResponse(): XAiAskUserQuestionCancelledResponse { return { outcome: "cancelled" };}
import type { ProviderUserInputAnswers, UserInputQuestion } from "@t3tools/contracts";
import * as Schema from "effect/Schema";
const XAiAskUserQuestionOption = Schema.Struct({
label: Schema.String,
description: Schema.optional(Schema.String),
preview: Schema.optional(Schema.String),
id: Schema.optional(Schema.String),
});
const XAiAskUserQuestion = Schema.Struct({
id: Schema.optional(Schema.String),
question: Schema.String,
options: Schema.Array(XAiAskUserQuestionOption),
multiSelect: Schema.optional(Schema.NullOr(Schema.Boolean)),
});
const XAiAskUserQuestionParams = Schema.Struct({
sessionId: Schema.String,
toolCallId: Schema.String,
questions: Schema.Array(XAiAskUserQuestion),
mode: Schema.Literals(["default", "plan"]),
});
const XAiWrappedAskUserQuestionParams = Schema.Struct({
method: Schema.Literals(["x.ai/ask_user_question", "_x.ai/ask_user_question"]),
params: XAiAskUserQuestionParams,
});
export const XAiAskUserQuestionRequest = Schema.Union([
XAiAskUserQuestionParams,
XAiWrappedAskUserQuestionParams,
]);
type XAiAskUserQuestionRequestParams = typeof XAiAskUserQuestionParams.Type;
type XAiAskUserQuestionRequest = typeof XAiAskUserQuestionRequest.Type;
function trimmed(value: string | undefined): string | undefined {
const text = value?.trim();
return text && text.length > 0 ? text : undefined;
}
function unwrapAskUserQuestionParams(
params: XAiAskUserQuestionRequest,
): XAiAskUserQuestionRequestParams {
return "params" in params ? params.params : params;
}
export function extractXAiAskUserQuestions(
params: XAiAskUserQuestionRequest,
): ReadonlyArray<UserInputQuestion> {
return unwrapAskUserQuestionParams(params).questions.map((question) => ({
id: question.id ?? question.question,
header: "Question",
question: question.question,
multiSelect: question.multiSelect === true,
options:
question.options.length > 0
? question.options.map((option) => ({
label: option.label,
description: option.description ?? option.label,
}))
: [{ label: "OK", description: "Continue" }],
}));
}
interface XAiAskUserQuestionAnnotation {
readonly preview?: string;
readonly notes?: string;
}
interface XAiAskUserQuestionAcceptedResponse {
readonly outcome: "accepted";
readonly answers: Record<string, ReadonlyArray<string>>;
readonly annotations?: Record<string, XAiAskUserQuestionAnnotation>;
}
interface XAiAskUserQuestionCancelledResponse {
readonly outcome: "cancelled";
}
export type XAiAskUserQuestionResponse =
| XAiAskUserQuestionAcceptedResponse
| XAiAskUserQuestionCancelledResponse;
interface NormalizedXAiAnswer {
readonly questionText: string;
readonly selectedLabels: ReadonlyArray<string>;
readonly annotation?: XAiAskUserQuestionAnnotation;
}
function answerValues(answer: unknown): ReadonlyArray<string> {
if (Array.isArray(answer)) {
return answer.flatMap((entry) => {
const text = typeof entry === "string" ? trimmed(entry) : undefined;
return text ? [text] : [];
});
}
const text = typeof answer === "string" ? trimmed(answer) : undefined;
return text ? [text] : [];
}
function normalizeAnswerForXAi(
question: XAiAskUserQuestionRequestParams["questions"][number],
answer: unknown,
): NormalizedXAiAnswer | undefined {
const values = answerValues(answer);
if (values.length === 0) {
return undefined;
}
const optionByLabel = new Map(question.options.map((option) => [option.label, option]));
const resolvedValues = values.map((value) => ({
value,
option: optionByLabel.get(value),
}));
const selectedLabels = resolvedValues.flatMap(({ option }) => (option ? [option.label] : []));
const notes = resolvedValues.flatMap(({ option, value }) => (option ? [] : [value]));
const preview =
question.multiSelect === true
? undefined
: resolvedValues.map(({ option }) => trimmed(option?.preview)).find((value) => value);
const annotation =
preview || notes.length > 0
? {
...(preview ? { preview } : {}),
...(notes.length > 0 ? { notes: notes.join("\n") } : {}),
}
: undefined;
return {
questionText: question.question,
selectedLabels: selectedLabels.length > 0 ? selectedLabels : ["Other"],
...(annotation ? { annotation } : {}),
};
}
function findQuestionAnswer(
answers: ProviderUserInputAnswers,
question: XAiAskUserQuestionRequestParams["questions"][number],
): unknown {
const key = question.id ?? question.question;
return answers[key] ?? answers[question.question];
}
export function makeXAiAskUserQuestionResponse(
params: XAiAskUserQuestionRequest,
answers: ProviderUserInputAnswers,
): XAiAskUserQuestionAcceptedResponse {
const questions = unwrapAskUserQuestionParams(params).questions;
const normalized = questions.flatMap((question) => {
const entry = normalizeAnswerForXAi(question, findQuestionAnswer(answers, question));
return entry ? [entry] : [];
});
const annotations = Object.fromEntries(
normalized.flatMap((entry) =>
entry.annotation ? [[entry.questionText, entry.annotation] as const] : [],
),
);
return {
outcome: "accepted",
answers: Object.fromEntries(
normalized.map((entry) => [entry.questionText, entry.selectedLabels]),
),
...(Object.keys(annotations).length > 0 ? { annotations } : {}),
};
}
export function makeXAiAskUserQuestionCancelledResponse(): XAiAskUserQuestionCancelledResponse {
return { outcome: "cancelled" };
}