Skip to content
Merged
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
78 changes: 42 additions & 36 deletions src/CodexElicitationHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,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: ";

function normalizeElicitationSchema(value: unknown): acp.ElicitationSchema {
const normalized = normalizeElicitationSchemaValue(value);
Expand Down Expand Up @@ -109,17 +111,14 @@ function elicitationResponseMeta(
return Object.keys(meta).length === 0 ? null : meta;
}

function userInputOtherFieldId(questionId: string, questionIds: Set<string>): string {
const base = `${questionId}${USER_INPUT_OTHER_FIELD_SUFFIX}`;
if (!questionIds.has(base)) {
return base;
}

function userInputNoteFieldId(questionId: string, questionIds: ReadonlySet<string>): string {
const base = `${questionId}${USER_INPUT_NOTE_FIELD_SUFFIX}`;
let fieldId = base;
let index = 1;
while (questionIds.has(`${base}${index}`)) {
index += 1;
while (questionIds.has(fieldId)) {
fieldId = `${base}${index++}`;
}
return `${base}${index}`;
return fieldId;
}

function userInputResponseValue(
Expand Down Expand Up @@ -400,56 +399,57 @@ 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: {
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 && !options.some(option => option.label === USER_INPUT_OTHER_OPTION) ? [{
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,
},
},
};
}
}

const firstQuestion = params.questions[0];
return {
sessionId: params.threadId,
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,
Expand Down Expand Up @@ -523,17 +523,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 };
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
{
"method": "createElicitation",
"args": [
{
"sessionId": "test-session-id",
"toolCallId": "request-user-input-1",
"mode": "form",
"message": "Codex needs your input to continue.",
"requestedSchema": {
"type": "object",
"properties": {
"next_step": {
"title": "What should I do next?",
"description": "Next step",
"_meta": {
"codex": {
"isOther": true,
"isSecret": false
}
},
"type": "string",
"oneOf": [
{
"const": "Run tests",
"title": "Run tests",
"description": "Run the focused test suite."
},
{
"const": "None of the above",
"title": "None of the above",
"description": "Use a different approach."
}
]
},
"next_step_note": {
"type": "string",
"title": "Additional answer or note",
"_meta": {
"codex": {
"questionId": "next_step",
"role": "user_note",
"isSecret": false
}
}
}
},
"required": [
"next_step"
]
},
"_meta": {
"codex": {
"autoResolutionMs": null
}
}
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
{
"method": "createElicitation",
"args": [
{
"sessionId": "test-session-id",
"toolCallId": "request-user-input-1",
"mode": "form",
"message": "Codex needs your input to continue.",
"requestedSchema": {
"type": "object",
"properties": {
"choice": {
"title": "What should I do next?",
"description": "Next step",
"_meta": {
"codex": {
"isOther": true,
"isSecret": true
}
},
"type": "string",
"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."
}
]
},
"choice_note2": {
"type": "string",
"title": "Additional answer or note",
"_meta": {
"codex": {
"questionId": "choice",
"role": "user_note",
"isSecret": true
}
}
},
"choice_note": {
"title": "Which constraints should I follow?",
"description": "Constraints",
"_meta": {
"codex": {
"isOther": false,
"isSecret": false
}
},
"type": "string"
},
"choice_note1": {
"title": "What private context should I consider?",
"description": "Private",
"_meta": {
"codex": {
"isOther": false,
"isSecret": true
}
},
"type": "string"
}
},
"required": [
"choice",
"choice_note",
"choice_note1"
]
},
"_meta": {
"codex": {
"autoResolutionMs": null
}
}
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
{
"method": "createElicitation",
"args": [
{
"sessionId": "test-session-id",
"toolCallId": "request-user-input-1",
"mode": "form",
"message": "Codex needs your input to continue.",
"requestedSchema": {
"type": "object",
"properties": {
"choice_note": {
"title": "Which constraints should I follow?",
"description": "Constraints",
"_meta": {
"codex": {
"isOther": false,
"isSecret": false
}
},
"type": "string"
},
"choice_note1": {
"title": "What private context should I consider?",
"description": "Private",
"_meta": {
"codex": {
"isOther": false,
"isSecret": true
}
},
"type": "string"
},
"choice": {
"title": "What should I do next?",
"description": "Next step",
"_meta": {
"codex": {
"isOther": true,
"isSecret": true
}
},
"type": "string",
"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."
}
]
},
"choice_note2": {
"type": "string",
"title": "Additional answer or note",
"_meta": {
"codex": {
"questionId": "choice",
"role": "user_note",
"isSecret": true
}
}
}
},
"required": [
"choice_note",
"choice_note1",
"choice"
]
},
"_meta": {
"codex": {
"autoResolutionMs": null
}
}
}
]
}
Loading
Loading