fix: remove native question limits

This commit is contained in:
mesalogo
2026-09-08 21:52:10 +08:00
parent fbc0f5e9f3
commit 78418a56fa
6 changed files with 75 additions and 38 deletions
+5 -6
View File
@@ -120,14 +120,13 @@ const continueHostStreamEventSchema = z.discriminatedUnion('type', [
const continueHostQuestionSchema = z
.object({
requestId: z.string().min(1).max(128),
requestId: z.string().min(1),
timestamp: z.number().finite().optional(),
question: z.string().trim().min(1).max(2_000),
question: z.string().trim().min(1),
options: z
.array(z.string().trim().min(1).max(200))
.max(20)
.array(z.string().trim().min(1))
.optional(),
defaultAnswer: z.string().trim().max(2_000).optional()
defaultAnswer: z.string().trim().optional()
})
.strict()
@@ -925,7 +924,7 @@ export class ContinueHostAdapter {
patched = replaceExactly(
patched,
serverMarker,
'let j=(0,atn.default)();if(!process.env.GOODBUDDY_CONTINUE_HOST_TOKEN)throw new Error("Missing GoodBuddy host token");j.use((we,Te,ue)=>{we.headers.authorization===`Bearer ${process.env.GOODBUDDY_CONTINUE_HOST_TOKEN}`?ue():Te.status(401).json({error:"Unauthorized"})}),j.use(atn.default.json({limit:"20mb"})),j.post("/goodbuddy/question-answer",(we,Te)=>{let{requestId:ue,answer:ce,isCustomAnswer:de}=we.body??{};typeof ue==="string"&&ue.length>0&&ue.length<=128&&typeof ce==="string"&&ce.length>0&&ce.length<=2e3?Lbe.answerQuestion(ue,ce,de===!0)?Te.json({success:!0}):Te.status(404).json({error:"Question not pending"}):Te.status(400).json({error:"Invalid question answer"})}),j.get("/state"'
'let j=(0,atn.default)();if(!process.env.GOODBUDDY_CONTINUE_HOST_TOKEN)throw new Error("Missing GoodBuddy host token");j.use((we,Te,ue)=>{we.headers.authorization===`Bearer ${process.env.GOODBUDDY_CONTINUE_HOST_TOKEN}`?ue():Te.status(401).json({error:"Unauthorized"})}),j.use(atn.default.json({limit:"20mb"})),j.post("/goodbuddy/question-answer",(we,Te)=>{let{requestId:ue,answer:ce,isCustomAnswer:de}=we.body??{};typeof ue==="string"&&ue.length>0&&typeof ce==="string"&&ce.length>0?Lbe.answerQuestion(ue,ce,de===!0)?Te.json({success:!0}):Te.status(404).json({error:"Question not pending"}):Te.status(400).json({error:"Invalid question answer"})}),j.get("/state"'
)
patched = replaceExactly(
patched,
+40 -2
View File
@@ -2075,7 +2075,26 @@ describe("OpenCodeRuntime embedded permission mediation", () => {
);
it.each(["parent", "child"] as const)("parses %s OpenCode questions and sends selected answers back", async (owner) => {
const longQuestion = `请选择实现方式\n${"发布说明内容。".repeat(6_000)}`;
const setup = runClient([
{
id: "question-tool-running",
type: "message.part.updated",
properties: {
sessionID: "session-1",
part: {
id: "part-question-1",
callID: "call-question-1",
type: "tool",
tool: "question",
state: {
status: "running",
input: { questions: [] },
time: { start: 1 },
},
},
},
},
{
id: "question-event",
type: "question.asked",
@@ -2085,7 +2104,7 @@ describe("OpenCodeRuntime embedded permission mediation", () => {
questions: [
{
header: "实现方式",
question: "请选择实现方式",
question: longQuestion,
options: [
{
label: "直接修改",
@@ -2106,6 +2125,25 @@ describe("OpenCodeRuntime embedded permission mediation", () => {
},
},
},
{
id: "question-tool-completed",
type: "message.part.updated",
properties: {
sessionID: "session-1",
part: {
id: "part-question-1",
callID: "call-question-1",
type: "tool",
tool: "question",
state: {
status: "completed",
input: { questions: [] },
output: "Answered",
time: { start: 1, end: 2 },
},
},
},
},
{
id: "idle",
type: "session.idle",
@@ -2136,7 +2174,7 @@ describe("OpenCodeRuntime embedded permission mediation", () => {
questions: [
{
header: "实现方式",
question: "请选择实现方式",
question: longQuestion,
multiple: false,
custom: true,
},
+4 -24
View File
@@ -81,9 +81,6 @@ const MAX_PERMISSION_PATTERN_LENGTH = 1_024;
const MAX_PERMISSION_PATTERNS_BYTES = 8 * 1_024;
const MAX_PERMISSION_METADATA_BYTES = 8 * 1_024;
const MAX_EXECUTION_OUTPUT_BYTES = 1024 * 1024;
const MAX_QUESTION_REQUEST_BYTES = 32 * 1_024;
const MAX_QUESTIONS_PER_REQUEST = 4;
const MAX_QUESTION_OPTIONS = 20;
const MAX_NATIVE_AGENTS = runtimeNativeInventoryLimits.agents;
const MAX_NATIVE_TOOLS = runtimeNativeInventoryLimits.tools;
const MAX_NATIVE_COMMANDS = runtimeNativeInventoryLimits.commands;
@@ -384,29 +381,22 @@ function parseQuestionRequest(
if (
typeof id !== "string" ||
id.length === 0 ||
id.length > MAX_PERMISSION_NAME_LENGTH ||
!Array.isArray(questions) ||
questions.length === 0 ||
questions.length > MAX_QUESTIONS_PER_REQUEST ||
!questions.every(
(question) =>
isRecord(question) &&
typeof question.question === "string" &&
question.question.trim().length > 0 &&
question.question.length <= 2_000 &&
typeof question.header === "string" &&
question.header.trim().length > 0 &&
question.header.length <= 120 &&
Array.isArray(question.options) &&
question.options.length <= MAX_QUESTION_OPTIONS &&
question.options.every(
(option) =>
isRecord(option) &&
typeof option.label === "string" &&
option.label.trim().length > 0 &&
option.label.length <= 200 &&
typeof option.description === "string" &&
option.description.length <= 1_000,
typeof option.description === "string",
) &&
(question.multiple === undefined ||
typeof question.multiple === "boolean") &&
@@ -416,22 +406,11 @@ function parseQuestionRequest(
(!isRecord(tool) ||
typeof tool.messageID !== "string" ||
tool.messageID.length === 0 ||
tool.messageID.length > 256 ||
typeof tool.callID !== "string" ||
tool.callID.length === 0 ||
tool.callID.length > 256))
tool.callID.length === 0))
) {
throw new Error("OpenCode 提问请求格式无效");
}
let serialized: string;
try {
serialized = JSON.stringify(properties);
} catch {
throw new Error("OpenCode 提问请求无法序列化");
}
if (!byteLengthWithin(serialized, MAX_QUESTION_REQUEST_BYTES)) {
throw new Error("OpenCode 提问请求超过安全限制");
}
return properties as QuestionRequest;
}
@@ -2569,6 +2548,7 @@ export class OpenCodeRuntime implements AgentRuntime {
throw new Error("OpenCode 工具调用 ID 格式无效");
}
const toolName = part.tool.slice(0, 200);
const nativeControlTool = toolName.trim().toLowerCase() === "question";
const state =
part.state.status === "error" ? "failed" : part.state.status;
if (state === "failed") {
@@ -2609,7 +2589,7 @@ export class OpenCodeRuntime implements AgentRuntime {
const retained = subagentProgress.retain(subagent);
toolStates.get(callId)!.subagent = retained;
yield retained;
} else {
} else if (!nativeControlTool) {
yield {
requestId: request.requestId,
type: "tool",
-1
View File
@@ -122,7 +122,6 @@ export function AgentQuestionCard({
<span>{t('question.otherAnswer')}</span>
<input
disabled={submitting}
maxLength={2_000}
onChange={(event) => {
const answer = event.target.value
setCustom((current) =>
+23 -1
View File
@@ -15,7 +15,10 @@ import {
projectExecutionSpaceSchema,
projectUpdateSchema
} from './assistant-contracts'
import { subagentEventSchema } from './contracts'
import {
agentQuestionResponseSchema,
subagentEventSchema
} from './contracts'
const untouchedProject: AssistantProject = {
id: '00000000-0000-4000-8000-000000000101',
@@ -34,6 +37,25 @@ const untouchedProject: AssistantProject = {
updatedAt: '2026-08-01T00:00:00.000Z'
}
describe('agent question contracts', () => {
it('does not impose GoodBuddy-specific question answer limits', () => {
const answers = Array.from({ length: 5 }, (_, questionIndex) =>
Array.from(
{ length: 21 },
(_, optionIndex) =>
`${questionIndex}:${optionIndex}:${'回答内容'.repeat(600)}`
)
)
expect(
agentQuestionResponseSchema.parse({
questionId: `question-${'id'.repeat(100)}`,
answers
}).answers
).toEqual(answers)
})
})
describe('activity history contracts', () => {
it('accepts more than 500 records and details longer than 4,000 characters', () => {
const records = Array.from({ length: 501 }, (_, index) => ({
+3 -4
View File
@@ -305,13 +305,12 @@ export const workspaceOpenPathRequestSchema = z
.strict()
export const agentQuestionAnswerSchema = z
.array(z.string().trim().min(1).max(2_000))
.max(20)
.array(z.string().trim().min(1))
export const agentQuestionResponseSchema = z
.object({
questionId: z.string().trim().min(1).max(128),
answers: z.array(agentQuestionAnswerSchema).max(4)
questionId: z.string().trim().min(1),
answers: z.array(agentQuestionAnswerSchema)
})
.strict()