* Bridge an SDK QuestionRequest (the AskUserQuestion tool's * reverse-RPC) through the same ACP * `session/request_permission` surface used by approvals. * * ACP currently has no dedicated `session/request_question` method, so * the adapter re-uses `requestPermission` and tags
(req: QuestionRequest)
| 1293 | * is strictly safer than fabricating an answer the user did not give. |
| 1294 | */ |
| 1295 | private async handleQuestion(req: QuestionRequest): Promise<QuestionAnswers | null> { |
| 1296 | const questions = req.questions; |
| 1297 | if (questions.length === 0) { |
| 1298 | // Pathological input — log and dismiss. No telemetry: the SDK |
| 1299 | // would never emit an empty `questions` payload in practice. |
| 1300 | log.warn('acp: handleQuestion received empty questions array', { |
| 1301 | sessionId: this.id, |
| 1302 | }); |
| 1303 | return null; |
| 1304 | } |
| 1305 | if (questions.length > 1) { |
| 1306 | log.warn('acp: handleQuestion degrading to first question only', { |
| 1307 | sessionId: this.id, |
| 1308 | dropped: questions.length - 1, |
| 1309 | }); |
| 1310 | this.emitTelemetry('question_degraded', { |
| 1311 | reason: 'multi_question', |
| 1312 | dropped: questions.length - 1, |
| 1313 | }); |
| 1314 | } |
| 1315 | const q = questions[0]!; |
| 1316 | if (q.multiSelect === true) { |
| 1317 | this.emitTelemetry('question_degraded', { reason: 'multi_select' }); |
| 1318 | } |
| 1319 | const options = questionItemToPermissionOptions(q, 0); |
| 1320 | const rawToolCallId = req.toolCallId ?? 'ask-user'; |
| 1321 | const toolCallId = |
| 1322 | this.currentTurnId !== undefined |
| 1323 | ? acpToolCallId(this.currentTurnId, rawToolCallId) |
| 1324 | : rawToolCallId; |
| 1325 | try { |
| 1326 | const response = await this.conn.requestPermission({ |
| 1327 | sessionId: this.id, |
| 1328 | options: [...options], |
| 1329 | toolCall: { |
| 1330 | toolCallId, |
| 1331 | title: 'AskUserQuestion', |
| 1332 | content: [{ type: 'content', content: { type: 'text', text: q.question } }], |
| 1333 | }, |
| 1334 | }); |
| 1335 | const answer = outcomeToQuestionAnswer(q, response); |
| 1336 | if (answer === null) { |
| 1337 | // Dismissed via skip / cancel / unknown optionId — telemetry |
| 1338 | // matches the ask-user tool's existing `question_dismissed` |
| 1339 | // event so dashboards stay coherent. |
| 1340 | this.emitTelemetry('question_dismissed'); |
| 1341 | } else { |
| 1342 | this.emitTelemetry('question_answered', { answered: Object.keys(answer).length }); |
| 1343 | } |
| 1344 | return answer; |
| 1345 | } catch (err) { |
| 1346 | log.warn('acp: requestPermission (question) failed; dismissing', { |
| 1347 | sessionId: this.id, |
| 1348 | toolCallId: req.toolCallId, |
| 1349 | error: err instanceof Error ? err.message : String(err), |
| 1350 | }); |
| 1351 | return null; |
| 1352 | } |
no test coverage detected