From 8ca15c15d4365ba9d87ceffb16583654b4ce162b Mon Sep 17 00:00:00 2001 From: Shushakov Daniil 84373458 Date: Tue, 14 Jul 2026 18:22:58 +0300 Subject: [PATCH 1/4] Fix request_user_input elicitation form structure --- src/CodexElicitationHandler.ts | 75 +++++++++++-------- .../CodexACPAgent/elicitation-events.test.ts | 67 +++++++++++------ 2 files changed, 88 insertions(+), 54 deletions(-) diff --git a/src/CodexElicitationHandler.ts b/src/CodexElicitationHandler.ts index b0ca0eab..132953e9 100644 --- a/src/CodexElicitationHandler.ts +++ b/src/CodexElicitationHandler.ts @@ -38,7 +38,9 @@ type AcpBackedMcpElicitationParams = Extract< { mode: "form" } | { mode: "url" } >; -const USER_INPUT_OTHER_FIELD_SUFFIX = "__other"; +const USER_INPUT_NOTE_FIELD_SUFFIX = "_note"; +const USER_INPUT_OTHER_OPTION = "None of the above"; +const USER_INPUT_NOTE_PREFIX = "user_note: "; /** * Parses the `persist` field from the elicitation request `_meta`. @@ -223,17 +225,17 @@ function elicitationResponseMeta( return Object.keys(meta).length === 0 ? null : meta; } -function userInputOtherFieldId(questionId: string, questionIds: Set): string { - const base = `${questionId}${USER_INPUT_OTHER_FIELD_SUFFIX}`; +function userInputNoteFieldId(questionId: string, questionIds: Set): string { + const base = `${questionId}${USER_INPUT_NOTE_FIELD_SUFFIX}`; if (!questionIds.has(base)) { return base; } - let index = 1; - while (questionIds.has(`${base}${index}`)) { + let index = 2; + while (questionIds.has(`${base}_${index}`)) { index += 1; } - return `${base}${index}`; + return `${base}_${index}`; } function userInputResponseValue( @@ -502,41 +504,46 @@ export class CodexElicitationHandler implements ElicitationHandler { const hasOptions = options.length > 0; const hasOtherAnswer = question.isOther && hasOptions; const base = { - title: question.header || question.id, - description: question.question, + title: question.question || question.header || question.id, + ...(question.header ? { description: question.header } : {}), _meta: { codex: { + questionId: question.id, isOther: question.isOther, isSecret: question.isSecret, }, }, }; - if (!hasOtherAnswer) { - required.push(question.id); - } + required.push(question.id); properties[question.id] = hasOptions ? { ...base, type: "string", - oneOf: options.map(option => ({ - const: option.label, - title: option.label, - description: option.description, - })), + oneOf: [ + ...options.map(option => ({ + const: option.label, + title: option.label, + ...(option.description ? { description: option.description } : {}), + })), + ...(hasOtherAnswer ? [{ + const: USER_INPUT_OTHER_OPTION, + title: USER_INPUT_OTHER_OPTION, + description: "Provide a different answer in the note field.", + }] : []), + ], } : { ...base, type: "string", }; if (hasOtherAnswer) { - properties[userInputOtherFieldId(question.id, questionIds)] = { + properties[userInputNoteFieldId(question.id, questionIds)] = { type: "string", - title: "Other", - description: "Type your own answer instead of choosing an option above.", + title: "Additional answer or note", _meta: { codex: { questionId: question.id, - isOtherAnswer: true, + role: "user_note", isSecret: question.isSecret, }, }, @@ -544,14 +551,11 @@ export class CodexElicitationHandler implements ElicitationHandler { } } - const firstQuestion = params.questions[0]; return { sessionId: this.sessionState.sessionId, toolCallId: params.itemId, mode: "form", - message: params.questions.length === 1 && firstQuestion - ? firstQuestion.question - : "Input requested", + message: "Codex needs your input to continue.", requestedSchema: { type: "object", properties, @@ -559,6 +563,7 @@ export class CodexElicitationHandler implements ElicitationHandler { }, _meta: { codex: { + kind: "request_user_input", autoResolutionMs: params.autoResolutionMs, }, }, @@ -698,17 +703,23 @@ export class CodexElicitationHandler implements ElicitationHandler { const content = contentRecord(response.content); const questionIds = new Set(params.questions.map(question => question.id)); for (const question of params.questions) { - const value = question.isOther && question.options != null && question.options.length > 0 - ? userInputResponseValue(content, userInputOtherFieldId(question.id, questionIds)) - ?? userInputResponseValue(content, question.id) - : userInputResponseValue(content, question.id); - if (value === undefined) { + const answerValues: string[] = []; + const value = userInputResponseValue(content, question.id); + if (value !== undefined) { + answerValues.push(...(Array.isArray(value) ? value.map(String) : [String(value)])); + } + if (question.isOther && question.options != null && question.options.length > 0) { + const note = userInputResponseValue(content, userInputNoteFieldId(question.id, questionIds)); + if (note !== undefined) { + const notes = Array.isArray(note) ? note : [note]; + answerValues.push(...notes.map(item => `${USER_INPUT_NOTE_PREFIX}${String(item).trim()}`)); + } + } + if (answerValues.length === 0) { continue; } answers[question.id] = { - answers: Array.isArray(value) - ? value.map(String) - : [String(value)], + answers: answerValues, }; } return { answers }; diff --git a/src/__tests__/CodexACPAgent/elicitation-events.test.ts b/src/__tests__/CodexACPAgent/elicitation-events.test.ts index 57513621..b750b461 100644 --- a/src/__tests__/CodexACPAgent/elicitation-events.test.ts +++ b/src/__tests__/CodexACPAgent/elicitation-events.test.ts @@ -727,47 +727,70 @@ describe('Elicitation Events', () => { }, }); - const [elicitationEvent] = fixture.getAcpConnectionEvents(['_meta']); - expect(elicitationEvent).toMatchObject({ + expect(fixture.getAcpConnectionEvents([])).toEqual([{ method: 'createElicitation', args: [{ sessionId, toolCallId: 'request-user-input-1', mode: 'form', - message: 'Input requested', + message: 'Codex needs your input to continue.', requestedSchema: { type: 'object', - required: ['notes'], + properties: { + next_step: { + type: 'string', + title: 'What should I do next?', + description: 'Next step', + oneOf: [ + { const: 'Run tests', title: 'Run tests', description: 'Run the focused test suite.' }, + { const: 'Stop', title: 'Stop', description: 'Stop and report current status.' }, + { + const: 'None of the above', + title: 'None of the above', + description: 'Provide a different answer in the note field.', + }, + ], + _meta: { + codex: { questionId: 'next_step', isOther: true, isSecret: false }, + }, + }, + next_step_note: { + type: 'string', + title: 'Additional answer or note', + _meta: { + codex: { questionId: 'next_step', role: 'user_note', isSecret: false }, + }, + }, + notes: { + type: 'string', + title: 'Any extra instructions?', + description: 'Notes', + _meta: { + codex: { questionId: 'notes', isOther: false, isSecret: false }, + }, + }, + }, + required: ['next_step', 'notes'], + }, + _meta: { + codex: { kind: 'request_user_input', autoResolutionMs: 60000 }, }, }], - }); - expect(elicitationEvent!.args[0].requestedSchema.properties.next_step.oneOf).toEqual([ - { const: 'Run tests', title: 'Run tests', description: 'Run the focused test suite.' }, - { const: 'Stop', title: 'Stop', description: 'Stop and report current status.' }, - ]); - expect(elicitationEvent!.args[0].requestedSchema.properties.next_step__other).toMatchObject({ - type: 'string', - title: 'Other', - }); - expect(elicitationEvent!.args[0].requestedSchema.properties.notes).toMatchObject({ - type: 'string', - title: 'Notes', - description: 'Any extra instructions?', - }); + }]); completeTurn(); await promptPromise; }); - it('should prefer free-form Other answers over fixed choices', async () => { + it('should return the Other choice and its note using Codex answer conventions', async () => { const { promptPromise, completeTurn } = await setupSessionWithPendingPromptAndCapabilities({ elicitation: { form: {} }, }); fixture.setElicitationResponse({ action: 'accept', content: { - next_step: 'Run tests', - next_step__other: 'Inspect flaky logs', + next_step: 'None of the above', + next_step_note: 'Inspect flaky logs', }, }); @@ -792,7 +815,7 @@ describe('Elicitation Events', () => { const response = await fixture.sendServerRequest('item/tool/requestUserInput', params); expect(response).toEqual({ answers: { - next_step: { answers: ['Inspect flaky logs'] }, + next_step: { answers: ['None of the above', 'user_note: Inspect flaky logs'] }, }, }); From 436a5ab0e9c7e878620e243279be99eae9726fec Mon Sep 17 00:00:00 2001 From: Daniil Shushakov <4shushakov@gmail.com> Date: Tue, 14 Jul 2026 22:53:25 +0300 Subject: [PATCH 2/4] Simplify request_user_input note field IDs --- src/CodexElicitationHandler.ts | 20 ++------------------ 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/src/CodexElicitationHandler.ts b/src/CodexElicitationHandler.ts index 132953e9..4627e511 100644 --- a/src/CodexElicitationHandler.ts +++ b/src/CodexElicitationHandler.ts @@ -225,19 +225,6 @@ function elicitationResponseMeta( return Object.keys(meta).length === 0 ? null : meta; } -function userInputNoteFieldId(questionId: string, questionIds: Set): string { - const base = `${questionId}${USER_INPUT_NOTE_FIELD_SUFFIX}`; - if (!questionIds.has(base)) { - return base; - } - - let index = 2; - while (questionIds.has(`${base}_${index}`)) { - index += 1; - } - return `${base}_${index}`; -} - function userInputResponseValue( content: Record, fieldId: string @@ -497,8 +484,6 @@ export class CodexElicitationHandler implements ElicitationHandler { private buildUserInputRequest(params: ToolRequestUserInputParams): acp.CreateElicitationRequest { const properties: Record = {}; const required: string[] = []; - const questionIds = new Set(params.questions.map(question => question.id)); - for (const question of params.questions) { const options = question.options ?? []; const hasOptions = options.length > 0; @@ -537,7 +522,7 @@ export class CodexElicitationHandler implements ElicitationHandler { type: "string", }; if (hasOtherAnswer) { - properties[userInputNoteFieldId(question.id, questionIds)] = { + properties[`${question.id}${USER_INPUT_NOTE_FIELD_SUFFIX}`] = { type: "string", title: "Additional answer or note", _meta: { @@ -701,7 +686,6 @@ export class CodexElicitationHandler implements ElicitationHandler { const answers: ToolRequestUserInputResponse["answers"] = {}; const content = contentRecord(response.content); - const questionIds = new Set(params.questions.map(question => question.id)); for (const question of params.questions) { const answerValues: string[] = []; const value = userInputResponseValue(content, question.id); @@ -709,7 +693,7 @@ export class CodexElicitationHandler implements ElicitationHandler { answerValues.push(...(Array.isArray(value) ? value.map(String) : [String(value)])); } if (question.isOther && question.options != null && question.options.length > 0) { - const note = userInputResponseValue(content, userInputNoteFieldId(question.id, questionIds)); + const note = userInputResponseValue(content, `${question.id}${USER_INPUT_NOTE_FIELD_SUFFIX}`); if (note !== undefined) { const notes = Array.isArray(note) ? note : [note]; answerValues.push(...notes.map(item => `${USER_INPUT_NOTE_PREFIX}${String(item).trim()}`)); From b530fadf2f333f2991753c13558d592e73ae63b4 Mon Sep 17 00:00:00 2001 From: Daniil Shushakov <4shushakov@gmail.com> Date: Tue, 14 Jul 2026 23:03:37 +0300 Subject: [PATCH 3/4] Remove redundant question IDs from form fields --- src/CodexElicitationHandler.ts | 1 - src/__tests__/CodexACPAgent/elicitation-events.test.ts | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/CodexElicitationHandler.ts b/src/CodexElicitationHandler.ts index 4627e511..6b547245 100644 --- a/src/CodexElicitationHandler.ts +++ b/src/CodexElicitationHandler.ts @@ -493,7 +493,6 @@ export class CodexElicitationHandler implements ElicitationHandler { ...(question.header ? { description: question.header } : {}), _meta: { codex: { - questionId: question.id, isOther: question.isOther, isSecret: question.isSecret, }, diff --git a/src/__tests__/CodexACPAgent/elicitation-events.test.ts b/src/__tests__/CodexACPAgent/elicitation-events.test.ts index b750b461..315d0c2b 100644 --- a/src/__tests__/CodexACPAgent/elicitation-events.test.ts +++ b/src/__tests__/CodexACPAgent/elicitation-events.test.ts @@ -751,7 +751,7 @@ describe('Elicitation Events', () => { }, ], _meta: { - codex: { questionId: 'next_step', isOther: true, isSecret: false }, + codex: { isOther: true, isSecret: false }, }, }, next_step_note: { @@ -766,7 +766,7 @@ describe('Elicitation Events', () => { title: 'Any extra instructions?', description: 'Notes', _meta: { - codex: { questionId: 'notes', isOther: false, isSecret: false }, + codex: { isOther: false, isSecret: false }, }, }, }, From cf77ce8558b550ac8312084738802c665509ba66 Mon Sep 17 00:00:00 2001 From: Daniil Shushakov <4shushakov@gmail.com> Date: Tue, 14 Jul 2026 23:17:16 +0300 Subject: [PATCH 4/4] Remove unused request input metadata --- src/CodexElicitationHandler.ts | 1 - src/__tests__/CodexACPAgent/elicitation-events.test.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/CodexElicitationHandler.ts b/src/CodexElicitationHandler.ts index 6b547245..2e633306 100644 --- a/src/CodexElicitationHandler.ts +++ b/src/CodexElicitationHandler.ts @@ -547,7 +547,6 @@ export class CodexElicitationHandler implements ElicitationHandler { }, _meta: { codex: { - kind: "request_user_input", autoResolutionMs: params.autoResolutionMs, }, }, diff --git a/src/__tests__/CodexACPAgent/elicitation-events.test.ts b/src/__tests__/CodexACPAgent/elicitation-events.test.ts index 315d0c2b..05849b48 100644 --- a/src/__tests__/CodexACPAgent/elicitation-events.test.ts +++ b/src/__tests__/CodexACPAgent/elicitation-events.test.ts @@ -773,7 +773,7 @@ describe('Elicitation Events', () => { required: ['next_step', 'notes'], }, _meta: { - codex: { kind: 'request_user_input', autoResolutionMs: 60000 }, + codex: { autoResolutionMs: 60000 }, }, }], }]);