Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions src/browser/utils/messages/openAskUserQuestions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import {
getAskUserQuestionOccurrences,
isAskUserQuestionOccurrenceOpen,
} from "@/common/utils/tools/askUserQuestionSummary";
import { TOOL_DEFINITIONS } from "@/common/utils/tools/toolDefinitions";
// Not toolDefinitions.ts: this module is in the main chunk (via the aggregator), and that file
// would bring every tool schema with it (#7363).
import { AskUserQuestionToolArgsSchema } from "@/common/utils/tools/askUserQuestionSchemas";

// Which ask_user_question rows still take answers. One selector serves the dock above the chat
// input, the transcript stubs and transcript truncation (StreamingMessageAggregator keeps these
Expand Down Expand Up @@ -70,7 +72,7 @@ export type AskUserQuestionToolMessage = Extract<DisplayedMessage, { type: "tool
* releases itself from the dock, so they stay in the transcript (checked last: it parses args).
*/
function hasValidAskUserQuestionArgs(message: AskUserQuestionToolMessage): boolean {
return TOOL_DEFINITIONS.ask_user_question.schema.safeParse(message.args).success;
return AskUserQuestionToolArgsSchema.safeParse(message.args).success;
}

function isToolRow(message: DisplayedMessage): message is AskUserQuestionToolMessage {
Expand Down
61 changes: 61 additions & 0 deletions src/common/utils/tools/askUserQuestionSchemas.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { z } from "zod";

// The ask_user_question argument schemas, apart from toolDefinitions.ts (which re-exports them):
// the renderer's eager open-question selector (openAskUserQuestions.ts, used by
// StreamingMessageAggregator) parses args with AskUserQuestionToolArgsSchema, and importing
// toolDefinitions.ts there put every tool schema into the main chunk (#7363). Import only zod.

export const AskUserQuestionOptionSchema = z
.object({
label: z.string().min(1),
description: z.string().min(1),
})
.strict();

export const AskUserQuestionQuestionSchema = z
.object({
question: z.string().min(1),
header: z.string().min(1).max(32).describe("Short UI label"),
options: z.array(AskUserQuestionOptionSchema).min(2).max(4),
multiSelect: z.boolean(),
})
.strict()
.superRefine((question, ctx) => {
const labels = question.options.map((o) => o.label);
const labelSet = new Set(labels);
if (labelSet.size !== labels.length) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Option labels must be unique within a question",
path: ["options"],
});
}

// Claude Code provides "Other" automatically; do not include it explicitly.
if (labels.some((label) => label.trim().toLowerCase() === "other")) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Do not include an 'Other' option; it is provided automatically",
path: ["options"],
});
}
});

export const AskUserQuestionToolArgsSchema = z
.object({
questions: z.array(AskUserQuestionQuestionSchema).min(1).max(4),
// Optional prefilled answers (Claude Code supports this, though Xum typically won't use it)
answers: z.record(z.string(), z.string()).nullish(),
})
.strict()
.superRefine((args, ctx) => {
const questionTexts = args.questions.map((q) => q.question);
const questionTextSet = new Set(questionTexts);
if (questionTextSet.size !== questionTexts.length) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Question text must be unique across questions",
path: ["questions"],
});
}
});
8 changes: 8 additions & 0 deletions src/common/utils/tools/toolDefinitions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
TOOL_DEFINITIONS,
WorkflowRunToolArgsSchema,
} from "./toolDefinitions";
import { AskUserQuestionToolArgsSchema as LightAskUserQuestionToolArgsSchema } from "./askUserQuestionSchemas";

describe("TOOL_DEFINITIONS", () => {
it("accepts custom subagent_type IDs (deprecated alias)", () => {
Expand Down Expand Up @@ -578,6 +579,13 @@ describe("TOOL_DEFINITIONS", () => {
});
});

// The renderer's open-question selector (openAskUserQuestions.ts) and the aggregator's
// truncation rule parse args with the light module's schema (kept out of the main chunk,
// #7363); it must stay the schema the tool itself validates with.
it("ask_user_question validates with the light askUserQuestionSchemas module's schema", () => {
expect(TOOL_DEFINITIONS.ask_user_question.schema).toBe(LightAskUserQuestionToolArgsSchema);
});

it("accepts ask_user_question headers longer than 12 characters", () => {
const parsed = TOOL_DEFINITIONS.ask_user_question.schema.safeParse({
questions: [
Expand Down
64 changes: 10 additions & 54 deletions src/common/utils/tools/toolDefinitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,46 +100,21 @@ import {
INSTANCE_DISCOVERY_DEFAULT_LIMIT,
INSTANCE_DISCOVERY_MAX_LIMIT,
} from "@/constants/agentMessaging";
import {
AskUserQuestionOptionSchema,
AskUserQuestionQuestionSchema,
AskUserQuestionToolArgsSchema,
} from "./askUserQuestionSchemas";

// -----------------------------------------------------------------------------
// ask_user_question (interactive questions for top-level agents)
// -----------------------------------------------------------------------------

export const AskUserQuestionOptionSchema = z
.object({
label: z.string().min(1),
description: z.string().min(1),
})
.strict();

export const AskUserQuestionQuestionSchema = z
.object({
question: z.string().min(1),
header: z.string().min(1).max(32).describe("Short UI label"),
options: z.array(AskUserQuestionOptionSchema).min(2).max(4),
multiSelect: z.boolean(),
})
.strict()
.superRefine((question, ctx) => {
const labels = question.options.map((o) => o.label);
const labelSet = new Set(labels);
if (labelSet.size !== labels.length) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Option labels must be unique within a question",
path: ["options"],
});
}

// Claude Code provides "Other" automatically; do not include it explicitly.
if (labels.some((label) => label.trim().toLowerCase() === "other")) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Do not include an 'Other' option; it is provided automatically",
path: ["options"],
});
}
});
export {
AskUserQuestionOptionSchema,
AskUserQuestionQuestionSchema,
AskUserQuestionToolArgsSchema,
};

const AskUserQuestionUiOnlySchema = z.object({
questions: z.array(AskUserQuestionQuestionSchema),
Expand Down Expand Up @@ -171,25 +146,6 @@ const ToolOutputUiOnlyFieldSchema = {
ui_only: ToolOutputUiOnlySchema.optional(),
};

export const AskUserQuestionToolArgsSchema = z
.object({
questions: z.array(AskUserQuestionQuestionSchema).min(1).max(4),
// Optional prefilled answers (Claude Code supports this, though Xum typically won't use it)
answers: z.record(z.string(), z.string()).nullish(),
})
.strict()
.superRefine((args, ctx) => {
const questionTexts = args.questions.map((q) => q.question);
const questionTextSet = new Set(questionTexts);
if (questionTextSet.size !== questionTexts.length) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Question text must be unique across questions",
path: ["questions"],
});
}
});

const AskUserQuestionToolSummarySchema = z
.object({
summary: z.string(),
Expand Down
Loading