fix: remove native question limits
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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) => ({
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user