1> ## Documentation Index
2> Fetch the complete documentation index at: https://code.claude.com/docs/llms.txt
3> Use this file to discover all available pages before exploring further.
4
5# Обработка одобрений и пользовательского ввода
6
7> Выводите запросы на одобрение Claude и уточняющие вопросы пользователям, а затем возвращайте их решения в SDK.
8
9Во время работы над задачей Claude иногда нужно проверить информацию у пользователей. Ему может потребоваться разрешение перед удалением файлов или нужно спросить, какую базу данных использовать для нового проекта. Ваше приложение должно выводить эти запросы пользователям, чтобы Claude мог продолжить работу с их вводом.
10
11Claude запрашивает пользовательский ввод в двух ситуациях: когда ему нужно **разрешение на использование инструмента** (например, удаление файлов или запуск команд) и когда у него есть **уточняющие вопросы** (через инструмент `AskUserQuestion`). Оба случая запускают ваш callback `canUseTool`, который приостанавливает выполнение до получения ответа. Это отличается от обычных диалоговых ходов, где Claude завершает работу и ждёт вашего следующего сообщения.
12
13Для уточняющих вопросов Claude генерирует вопросы и варианты ответов. Ваша роль — представить их пользователям и вернуть их выборы. Вы не можете добавлять свои собственные вопросы в этот процесс; если вам нужно что-то спросить у пользователей, сделайте это отдельно в логике вашего приложения.
14
15Callback может оставаться в ожидании неопределённо долго. Выполнение остаётся приостановленным до возврата вашего callback, и SDK отменяет ожидание только при отмене самого запроса. Если пользователь может ответить дольше, чем ваш процесс может разумно оставаться запущенным, TypeScript SDK поддерживает [hook `defer`](/ru/hooks#defer-a-tool-call-for-later), который позволяет процессу выйти и возобновиться позже из сохранённой сессии; эта опция недоступна в Python SDK.
16
17Это руководство показывает, как обнаружить каждый тип запроса и ответить надлежащим образом.
18
19## Обнаружение, когда Claude нуждается в вводе
20
21Передайте callback `canUseTool` в параметры вашего запроса. Callback срабатывает всякий раз, когда Claude нуждается в пользовательском вводе, получая имя инструмента и ввод в качестве аргументов:
22
23<CodeGroup>
24 ```python Python theme={null}
25 async def handle_tool_request(tool_name, input_data, context):
26 # Запросить у пользователя и вернуть разрешение или отказ
27 ...
28
29
30 options = ClaudeAgentOptions(can_use_tool=handle_tool_request)
31 ```
32
33 ```typescript TypeScript theme={null}
34 async function handleToolRequest(toolName, input, options) {
35 // options включает { signal: AbortSignal, suggestions?: PermissionUpdate[] }
36 // Запросить у пользователя и вернуть разрешение или отказ
37 }
38
39 const options = { canUseTool: handleToolRequest };
40 ```
41</CodeGroup>
42
43Callback срабатывает в двух случаях:
44
451. **Инструмент требует одобрения**: Claude хочет использовать инструмент, который не одобрен автоматически [правилами разрешений](/ru/agent-sdk/permissions) или режимами. Проверьте `tool_name` на имя инструмента (например, `"Bash"`, `"Write"`).
462. **Claude задаёт вопрос**: Claude вызывает инструмент `AskUserQuestion`. Проверьте, равен ли `tool_name == "AskUserQuestion"`, чтобы обработать его иначе. Если вы указываете массив `tools`, включите `AskUserQuestion` для работы этого функционала. Подробнее см. [Обработка уточняющих вопросов](#handle-clarifying-questions).
47
48<Note>
49 Чтобы автоматически разрешить или отклонить инструменты без запроса пользователей, используйте [hooks](/ru/agent-sdk/hooks). Hooks выполняются перед `canUseTool` и могут разрешить, отклонить или изменить запросы на основе вашей собственной логики. Вы также можете использовать [hook `PermissionRequest`](/ru/agent-sdk/hooks#available-hooks) для отправки внешних уведомлений (Slack, email, push) когда Claude ждёт одобрения.
50</Note>
51
52## Обработка запросов на одобрение инструмента
53
54После передачи callback `canUseTool` в параметры вашего запроса он срабатывает, когда Claude хочет использовать инструмент, который не одобрен автоматически. Ваш callback получает три аргумента:
55
56| Аргумент | Описание |
57| ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
58| `toolName` | Имя инструмента, который Claude хочет использовать (например, `"Bash"`, `"Write"`, `"Edit"`) |
59| `input` | Параметры, которые Claude передаёт инструменту. Содержимое варьируется в зависимости от инструмента. |
60| `options` (TS) / `context` (Python) | Дополнительный контекст, включая опциональные `suggestions` (предложенные записи `PermissionUpdate` для избежания повторного запроса) и сигнал отмены. В TypeScript `signal` — это `AbortSignal`; в Python поле signal зарезервировано для будущего использования. Подробнее см. [`ToolPermissionContext`](/ru/agent-sdk/python#toolpermissioncontext) для Python. |
61
62Объект `input` содержит параметры, специфичные для инструмента. Распространённые примеры:
63
64| Инструмент | Поля ввода |
65| ---------- | --------------------------------------- |
66| `Bash` | `command`, `description`, `timeout` |
67| `Write` | `file_path`, `content` |
68| `Edit` | `file_path`, `old_string`, `new_string` |
69| `Read` | `file_path`, `offset`, `limit` |
70
71Полные схемы ввода см. в справочнике SDK: [Python](/ru/agent-sdk/python#tool-input%2Foutput-types) | [TypeScript](/ru/agent-sdk/typescript#tool-input-types).
72
73Вы можете отобразить эту информацию пользователю, чтобы он мог решить, разрешить или отклонить действие, а затем вернуть соответствующий ответ.
74
75Следующий пример просит Claude создать и удалить тестовый файл. Когда Claude пытается выполнить каждую операцию, callback выводит запрос инструмента в терминал и запрашивает одобрение y/n.
76
77<CodeGroup>
78 ```python Python theme={null}
79 import asyncio
80
81 from claude_agent_sdk import ClaudeAgentOptions, ResultMessage, query
82 from claude_agent_sdk.types import (
83 HookMatcher,
84 PermissionResultAllow,
85 PermissionResultDeny,
86 ToolPermissionContext,
87 )
88
89
90 async def can_use_tool(
91 tool_name: str, input_data: dict, context: ToolPermissionContext
92 ) -> PermissionResultAllow | PermissionResultDeny:
93 # Отобразить запрос инструмента
94 print(f"\nTool: {tool_name}")
95 if tool_name == "Bash":
96 print(f"Command: {input_data.get('command')}")
97 if input_data.get("description"):
98 print(f"Description: {input_data.get('description')}")
99 else:
100 print(f"Input: {input_data}")
101
102 # Получить одобрение пользователя
103 response = input("Allow this action? (y/n): ")
104
105 # Вернуть разрешение или отказ на основе ответа пользователя
106 if response.lower() == "y":
107 # Разрешить: инструмент выполняется с исходным (или изменённым) вводом
108 return PermissionResultAllow(updated_input=input_data)
109 else:
110 # Отклонить: инструмент не выполняется, Claude видит сообщение
111 return PermissionResultDeny(message="User denied this action")
112
113
114 # Требуемый обходной путь: фиктивный hook держит поток открытым для can_use_tool
115 async def dummy_hook(input_data, tool_use_id, context):
116 return {"continue_": True}
117
118
119 async def prompt_stream():
120 yield {
121 "type": "user",
122 "message": {
123 "role": "user",
124 "content": "Create a test file in /tmp and then delete it",
125 },
126 }
127
128
129 async def main():
130 async for message in query(
131 prompt=prompt_stream(),
132 options=ClaudeAgentOptions(
133 can_use_tool=can_use_tool,
134 hooks={"PreToolUse": [HookMatcher(matcher=None, hooks=[dummy_hook])]},
135 ),
136 ):
137 if isinstance(message, ResultMessage) and message.subtype == "success":
138 print(message.result)
139
140
141 asyncio.run(main())
142 ```
143
144 ```typescript TypeScript theme={null}
145 import { query } from "@anthropic-ai/claude-agent-sdk";
146 import * as readline from "readline";
147
148 // Вспомогательная функция для запроса ввода пользователя в терминале
149 function prompt(question: string): Promise<string> {
150 const rl = readline.createInterface({
151 input: process.stdin,
152 output: process.stdout
153 });
154 return new Promise((resolve) =>
155 rl.question(question, (answer) => {
156 rl.close();
157 resolve(answer);
158 })
159 );
160 }
161
162 for await (const message of query({
163 prompt: "Create a test file in /tmp and then delete it",
164 options: {
165 canUseTool: async (toolName, input) => {
166 // Отобразить запрос инструмента
167 console.log(`\nTool: ${toolName}`);
168 if (toolName === "Bash") {
169 console.log(`Command: ${input.command}`);
170 if (input.description) console.log(`Description: ${input.description}`);
171 } else {
172 console.log(`Input: ${JSON.stringify(input, null, 2)}`);
173 }
174
175 // Получить одобрение пользователя
176 const response = await prompt("Allow this action? (y/n): ");
177
178 // Вернуть разрешение или отказ на основе ответа пользователя
179 if (response.toLowerCase() === "y") {
180 // Разрешить: инструмент выполняется с исходным (или изменённым) вводом
181 return { behavior: "allow", updatedInput: input };
182 } else {
183 // Отклонить: инструмент не выполняется, Claude видит сообщение
184 return { behavior: "deny", message: "User denied this action" };
185 }
186 }
187 }
188 })) {
189 if ("result" in message) console.log(message.result);
190 }
191 ```
192</CodeGroup>
193
194<Note>
195 В Python `can_use_tool` требует [режима потоковой передачи](/ru/agent-sdk/streaming-vs-single-mode) и hook `PreToolUse`, который возвращает `{"continue_": True}` для сохранения потока открытым. Без этого hook поток закроется до того, как callback разрешения сможет быть вызван.
196</Note>
197
198Этот пример использует поток y/n, где любой ввод, отличный от `y`, рассматривается как отказ. На практике вы можете создать более богатый пользовательский интерфейс, который позволяет пользователям изменять запрос, предоставлять обратную связь или полностью перенаправлять Claude. Подробнее см. [Ответ на запросы инструментов](#respond-to-tool-requests).
199
200### Ответ на запросы инструментов
201
202Ваш callback возвращает один из двух типов ответов:
203
204| Ответ | Python | TypeScript |
205| ------------- | ------------------------------------------ | ------------------------------------- |
206| **Разрешить** | `PermissionResultAllow(updated_input=...)` | `{ behavior: "allow", updatedInput }` |
207| **Отклонить** | `PermissionResultDeny(message=...)` | `{ behavior: "deny", message }` |
208
209При разрешении передайте ввод инструмента (исходный или изменённый). При отклонении предоставьте сообщение, объясняющее причину. Claude видит это сообщение и может скорректировать свой подход.
210
211<CodeGroup>
212 ```python Python theme={null}
213 from claude_agent_sdk.types import PermissionResultAllow, PermissionResultDeny
214
215 # Разрешить выполнение инструмента
216 return PermissionResultAllow(updated_input=input_data)
217
218 # Заблокировать инструмент
219 return PermissionResultDeny(message="User rejected this action")
220 ```
221
222 ```typescript TypeScript theme={null}
223 // Разрешить выполнение инструмента
224 return { behavior: "allow", updatedInput: input };
225
226 // Заблокировать инструмент
227 return { behavior: "deny", message: "User rejected this action" };
228 ```
229</CodeGroup>
230
231Помимо разрешения или отклонения, вы можете изменить ввод инструмента или предоставить контекст, который помогает Claude скорректировать свой подход:
232
233* **Одобрить**: позволить инструменту выполниться так, как запросил Claude
234* **Одобрить с изменениями**: изменить ввод перед выполнением (например, санитизировать пути, добавить ограничения)
235* **Отклонить**: заблокировать инструмент и объяснить Claude причину
236* **Предложить альтернативу**: заблокировать, но направить Claude к тому, что хочет пользователь
237* **Полностью перенаправить**: использовать [потоковый ввод](/ru/agent-sdk/streaming-vs-single-mode) для отправки Claude совершенно новой инструкции
238
239<Tabs>
240 <Tab title="Одобрить">
241 Пользователь одобряет действие как есть. Пропустите `input` из вашего callback без изменений и инструмент выполнится ровно так, как запросил Claude.
242
243 <CodeGroup>
244 ```python Python theme={null}
245 async def can_use_tool(tool_name, input_data, context):
246 print(f"Claude wants to use {tool_name}")
247 approved = await ask_user("Allow this action?")
248
249 if approved:
250 return PermissionResultAllow(updated_input=input_data)
251 return PermissionResultDeny(message="User declined")
252 ```
253
254 ```typescript TypeScript theme={null}
255 canUseTool: async (toolName, input) => {
256 console.log(`Claude wants to use ${toolName}`);
257 const approved = await askUser("Allow this action?");
258
259 if (approved) {
260 return { behavior: "allow", updatedInput: input };
261 }
262 return { behavior: "deny", message: "User declined" };
263 };
264 ```
265 </CodeGroup>
266 </Tab>
267
268 <Tab title="Одобрить с изменениями">
269 Пользователь одобряет, но хочет сначала изменить запрос. Вы можете изменить ввод перед выполнением инструмента. Claude видит результат, но не сообщается, что вы что-то изменили. Полезно для санитизации параметров, добавления ограничений или ограничения доступа.
270
271 <CodeGroup>
272 ```python Python theme={null}
273 async def can_use_tool(tool_name, input_data, context):
274 if tool_name == "Bash":
275 # Пользователь одобрил, но ограничить все команды песочницей
276 sandboxed_input = {**input_data}
277 sandboxed_input["command"] = input_data["command"].replace(
278 "/tmp", "/tmp/sandbox"
279 )
280 return PermissionResultAllow(updated_input=sandboxed_input)
281 return PermissionResultAllow(updated_input=input_data)
282 ```
283
284 ```typescript TypeScript theme={null}
285 canUseTool: async (toolName, input) => {
286 if (toolName === "Bash") {
287 // Пользователь одобрил, но ограничить все команды песочницей
288 const sandboxedInput = {
289 ...input,
290 command: input.command.replace("/tmp", "/tmp/sandbox")
291 };
292 return { behavior: "allow", updatedInput: sandboxedInput };
293 }
294 return { behavior: "allow", updatedInput: input };
295 };
296 ```
297 </CodeGroup>
298 </Tab>
299
300 <Tab title="Отклонить">
301 Пользователь не хочет, чтобы это действие произошло. Заблокируйте инструмент и предоставьте сообщение, объясняющее причину. Claude видит это сообщение и может попробовать другой подход.
302
303 <CodeGroup>
304 ```python Python theme={null}
305 async def can_use_tool(tool_name, input_data, context):
306 approved = await ask_user(f"Allow {tool_name}?")
307
308 if not approved:
309 return PermissionResultDeny(message="User rejected this action")
310 return PermissionResultAllow(updated_input=input_data)
311 ```
312
313 ```typescript TypeScript theme={null}
314 canUseTool: async (toolName, input) => {
315 const approved = await askUser(`Allow ${toolName}?`);
316
317 if (!approved) {
318 return {
319 behavior: "deny",
320 message: "User rejected this action"
321 };
322 }
323 return { behavior: "allow", updatedInput: input };
324 };
325 ```
326 </CodeGroup>
327 </Tab>
328
329 <Tab title="Предложить альтернативу">
330 Пользователь не хочет это конкретное действие, но имеет другую идею. Заблокируйте инструмент и включите рекомендацию в ваше сообщение. Claude прочитает это и решит, как действовать на основе вашей обратной связи.
331
332 <CodeGroup>
333 ```python Python theme={null}
334 async def can_use_tool(tool_name, input_data, context):
335 if tool_name == "Bash" and "rm" in input_data.get("command", ""):
336 # Пользователь не хочет удалять, предложить архивирование вместо этого
337 return PermissionResultDeny(
338 message="User doesn't want to delete files. They asked if you could compress them into an archive instead."
339 )
340 return PermissionResultAllow(updated_input=input_data)
341 ```
342
343 ```typescript TypeScript theme={null}
344 canUseTool: async (toolName, input) => {
345 if (toolName === "Bash" && input.command.includes("rm")) {
346 // Пользователь не хочет удалять, предложить архивирование вместо этого
347 return {
348 behavior: "deny",
349 message:
350 "User doesn't want to delete files. They asked if you could compress them into an archive instead."
351 };
352 }
353 return { behavior: "allow", updatedInput: input };
354 };
355 ```
356 </CodeGroup>
357 </Tab>
358
359 <Tab title="Полностью перенаправить">
360 Для полного изменения направления (не просто подсказка), используйте [потоковый ввод](/ru/agent-sdk/streaming-vs-single-mode) для отправки Claude новой инструкции напрямую. Это обходит текущий запрос инструмента и даёт Claude совершенно новые инструкции для следования.
361 </Tab>
362</Tabs>
363
364## Обработка уточняющих вопросов
365
366Когда Claude нуждается в дополнительном направлении для задачи с несколькими допустимыми подходами, он вызывает инструмент `AskUserQuestion`. Это запускает ваш callback `canUseTool` с `toolName`, установленным на `AskUserQuestion`. Ввод содержит вопросы Claude в виде вариантов с множественным выбором, которые вы выводите пользователю и возвращаете их выборы.
367
368<Tip>
369 Уточняющие вопросы особенно распространены в [режиме `plan`](/ru/agent-sdk/permissions#plan-mode-plan), где Claude исследует кодовую базу и задаёт вопросы перед предложением плана. Это делает режим plan идеальным для интерактивных рабочих процессов, где вы хотите, чтобы Claude собрал требования перед внесением изменений.
370</Tip>
371
372Следующие шаги показывают, как обработать уточняющие вопросы:
373
374<Steps>
375 <Step title="Передайте callback canUseTool">
376 Передайте callback `canUseTool` в параметры вашего запроса. По умолчанию `AskUserQuestion` доступен. Если вы указываете массив `tools` для ограничения возможностей Claude (например, агент только для чтения с только `Read`, `Glob` и `Grep`), включите `AskUserQuestion` в этот массив. В противном случае Claude не сможет задавать уточняющие вопросы:
377
378 <CodeGroup>
379 ```python Python theme={null}
380 async for message in query(
381 prompt="Analyze this codebase",
382 options=ClaudeAgentOptions(
383 # Включите AskUserQuestion в ваш список инструментов
384 tools=["Read", "Glob", "Grep", "AskUserQuestion"],
385 can_use_tool=can_use_tool,
386 ),
387 ):
388 print(message)
389 ```
390
391 ```typescript TypeScript theme={null}
392 for await (const message of query({
393 prompt: "Analyze this codebase",
394 options: {
395 // Включите AskUserQuestion в ваш список инструментов
396 tools: ["Read", "Glob", "Grep", "AskUserQuestion"],
397 canUseTool: async (toolName, input) => {
398 // Обработайте уточняющие вопросы здесь
399 }
400 }
401 })) {
402 console.log(message);
403 }
404 ```
405 </CodeGroup>
406 </Step>
407
408 <Step title="Обнаружьте AskUserQuestion">
409 В вашем callback проверьте, равен ли `toolName` `AskUserQuestion`, чтобы обработать его иначе, чем другие инструменты:
410
411 <CodeGroup>
412 ```python Python theme={null}
413 async def can_use_tool(tool_name: str, input_data: dict, context):
414 if tool_name == "AskUserQuestion":
415 # Ваша реализация для сбора ответов от пользователя
416 return await handle_clarifying_questions(input_data)
417 # Обработайте другие инструменты нормально
418 return await prompt_for_approval(tool_name, input_data)
419 ```
420
421 ```typescript TypeScript theme={null}
422 canUseTool: async (toolName, input) => {
423 if (toolName === "AskUserQuestion") {
424 // Ваша реализация для сбора ответов от пользователя
425 return handleClarifyingQuestions(input);
426 }
427 // Обработайте другие инструменты нормально
428 return promptForApproval(toolName, input);
429 };
430 ```
431 </CodeGroup>
432 </Step>
433
434 <Step title="Разберите ввод вопроса">
435 Ввод содержит вопросы Claude в массиве `questions`. Каждый вопрос имеет `question` (текст для отображения), `options` (варианты выбора) и `multiSelect` (разрешены ли множественные выборы):
436
437 ```json theme={null}
438 {
439 "questions": [
440 {
441 "question": "How should I format the output?",
442 "header": "Format",
443 "options": [
444 { "label": "Summary", "description": "Brief overview" },
445 { "label": "Detailed", "description": "Full explanation" }
446 ],
447 "multiSelect": false
448 },
449 {
450 "question": "Which sections should I include?",
451 "header": "Sections",
452 "options": [
453 { "label": "Introduction", "description": "Opening context" },
454 { "label": "Conclusion", "description": "Final summary" }
455 ],
456 "multiSelect": true
457 }
458 ]
459 }
460 ```
461
462 Полное описание полей см. в [Формат вопроса](#question-format).
463 </Step>
464
465 <Step title="Соберите ответы от пользователя">
466 Представьте вопросы пользователю и соберите их выборы. Как вы это сделаете, зависит от вашего приложения: терминальный запрос, веб-форма, мобильный диалог и т. д.
467 </Step>
468
469 <Step title="Верните ответы Claude">
470 Создайте объект `answers` как запись, где каждый ключ — это текст `question`, а каждое значение — это `label` выбранного варианта:
471
472 | Из объекта вопроса | Используйте как |
473 | --------------------------------------------------------------- | --------------- |
474 | Поле `question` (например, `"How should I format the output?"`) | Ключ |
475 | Поле `label` выбранного варианта (например, `"Summary"`) | Значение |
476
477 Для вопросов с множественным выбором передайте массив меток или объедините их с `", "`. Если вы [поддерживаете свободный ввод текста](#support-free-text-input), используйте пользовательский текст пользователя как значение.
478
479 <CodeGroup>
480 ```python Python theme={null}
481 return PermissionResultAllow(
482 updated_input={
483 "questions": input_data.get("questions", []),
484 "answers": {
485 "How should I format the output?": "Summary",
486 "Which sections should I include?": ["Introduction", "Conclusion"],
487 },
488 }
489 )
490 ```
491
492 ```typescript TypeScript theme={null}
493 return {
494 behavior: "allow",
495 updatedInput: {
496 questions: input.questions,
497 answers: {
498 "How should I format the output?": "Summary",
499 "Which sections should I include?": "Introduction, Conclusion"
500 }
501 }
502 };
503 ```
504 </CodeGroup>
505 </Step>
506</Steps>
507
508### Формат вопроса
509
510Ввод содержит сгенерированные Claude вопросы в массиве `questions`. Каждый вопрос имеет эти поля:
511
512| Поле | Описание |
513| ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
514| `question` | Полный текст вопроса для отображения |
515| `header` | Короткая метка для вопроса (максимум 12 символов) |
516| `options` | Массив из 2-4 вариантов выбора, каждый с `label` и `description`. TypeScript: опционально `preview` (см. [ниже](#option-previews-type-script)) |
517| `multiSelect` | Если `true`, пользователи могут выбрать несколько вариантов |
518
519Структура, которую получает ваш callback:
520
521```json theme={null}
522{
523 "questions": [
524 {
525 "question": "How should I format the output?",
526 "header": "Format",
527 "options": [
528 { "label": "Summary", "description": "Brief overview of key points" },
529 { "label": "Detailed", "description": "Full explanation with examples" }
530 ],
531 "multiSelect": false
532 }
533 ]
534}
535```
536
537#### Предпросмотры вариантов (TypeScript)
538
539`toolConfig.askUserQuestion.previewFormat` добавляет поле `preview` к каждому варианту, чтобы ваше приложение могло показать визуальный макет рядом с меткой. Без этого параметра Claude не генерирует предпросмотры и поле отсутствует.
540
541| `previewFormat` | `preview` содержит |
542| :---------------------------- | :----------------------------------------------------------------------------------------------------------------- |
543| не установлено (по умолчанию) | Поле отсутствует. Claude не генерирует предпросмотры. |
544| `"markdown"` | ASCII-арт и блоки кода в ограде |
545| `"html"` | Стилизованный фрагмент `<div>` (SDK отклоняет `<script>`, `<style>` и `<!DOCTYPE>` перед запуском вашего callback) |
546
547Формат применяется ко всем вопросам в сессии. Claude включает `preview` на варианты, где визуальное сравнение помогает (выбор макета, цветовые схемы) и опускает его, где оно не помогает (да/нет подтверждения, только текстовые варианты). Проверьте `undefined` перед рендерингом.
548
549```typescript theme={null}
550import { query } from "@anthropic-ai/claude-agent-sdk";
551
552for await (const message of query({
553 prompt: "Help me choose a card layout",
554 options: {
555 toolConfig: {
556 askUserQuestion: { previewFormat: "html" }
557 },
558 canUseTool: async (toolName, input) => {
559 // input.questions[].options[].preview — это HTML-строка или undefined
560 return { behavior: "allow", updatedInput: input };
561 }
562 }
563})) {
564 // ...
565}
566```
567
568Вариант с HTML-предпросмотром:
569
570```json theme={null}
571{
572 "label": "Compact",
573 "description": "Title and metric value only",
574 "preview": "<div style=\"padding:12px;border:1px solid #ddd;border-radius:8px\"><div style=\"font-size:12px;color:#666\">Active users</div><div style=\"font-size:28px;font-weight:600\">1,284</div></div>"
575}
576```
577
578### Формат ответа
579
580Верните объект `answers`, сопоставляющий поле `question` каждого вопроса с `label` выбранного варианта:
581
582| Поле | Описание |
583| ----------- | ------------------------------------------------------------------------- |
584| `questions` | Пропустите исходный массив вопросов (требуется для обработки инструмента) |
585| `answers` | Объект, где ключи — это текст вопроса, а значения — это выбранные метки |
586
587Для вопросов с множественным выбором передайте массив меток или объедините их с `", "`. Для свободного ввода текста используйте пользовательский текст пользователя напрямую.
588
589```json theme={null}
590{
591 "questions": [
592 // ...
593 ],
594 "answers": {
595 "How should I format the output?": "Summary",
596 "Which sections should I include?": ["Introduction", "Conclusion"]
597 }
598}
599```
600
601#### Поддержка свободного ввода текста
602
603Предопределённые варианты Claude не всегда охватывают то, что хотят пользователи. Чтобы позволить пользователям вводить свой собственный ответ:
604
605* Отобразите дополнительный выбор "Other" после вариантов Claude, который принимает текстовый ввод
606* Используйте пользовательский текст пользователя как значение ответа (не слово "Other")
607
608Полную реализацию см. в [полном примере](#complete-example) ниже.
609
610### Полный пример
611
612Claude задаёт уточняющие вопросы, когда ему нужен пользовательский ввод для продолжения. Например, когда его просят помочь решить, какой технологический стек использовать для мобильного приложения, Claude может спросить о кроссплатформенности vs нативности, предпочтениях бэкенда или целевых платформах. Эти вопросы помогают Claude принимать решения, которые соответствуют предпочтениям пользователя, а не угадывать.
613
614Этот пример обрабатывает эти вопросы в терминальном приложении. Вот что происходит на каждом шаге:
615
6161. **Маршрутизация запроса**: callback `canUseTool` проверяет, равно ли имя инструмента `"AskUserQuestion"` и маршрутизирует к выделенному обработчику
6172. **Отображение вопросов**: обработчик проходит по массиву `questions` и выводит каждый вопрос с пронумерованными вариантами
6183. **Сбор ввода**: пользователь может ввести номер для выбора варианта или ввести свободный текст напрямую (например, "jquery", "i don't know")
6194. **Сопоставление ответов**: код проверяет, является ли ввод числовым (использует метку варианта) или свободным текстом (использует текст напрямую)
6205. **Возврат Claude**: ответ включает как исходный массив `questions`, так и сопоставление `answers`
621
622<CodeGroup>
623 ```python Python theme={null}
624 import asyncio
625
626 from claude_agent_sdk import ClaudeAgentOptions, ResultMessage, query
627 from claude_agent_sdk.types import HookMatcher, PermissionResultAllow
628
629
630 def parse_response(response: str, options: list) -> str:
631 """Разберите пользовательский ввод как номер(а) варианта или свободный текст."""
632 try:
633 indices = [int(s.strip()) - 1 for s in response.split(",")]
634 labels = [options[i]["label"] for i in indices if 0 <= i < len(options)]
635 return ", ".join(labels) if labels else response
636 except ValueError:
637 return response
638
639
640 async def handle_ask_user_question(input_data: dict) -> PermissionResultAllow:
641 """Отобразите вопросы Claude и соберите ответы пользователя."""
642 answers = {}
643
644 for q in input_data.get("questions", []):
645 print(f"\n{q['header']}: {q['question']}")
646
647 options = q["options"]
648 for i, opt in enumerate(options):
649 print(f" {i + 1}. {opt['label']} - {opt['description']}")
650 if q.get("multiSelect"):
651 print(" (Enter numbers separated by commas, or type your own answer)")
652 else:
653 print(" (Enter a number, or type your own answer)")
654
655 response = input("Your choice: ").strip()
656 answers[q["question"]] = parse_response(response, options)
657
658 return PermissionResultAllow(
659 updated_input={
660 "questions": input_data.get("questions", []),
661 "answers": answers,
662 }
663 )
664
665
666 async def can_use_tool(
667 tool_name: str, input_data: dict, context
668 ) -> PermissionResultAllow:
669 # Маршрутизируйте AskUserQuestion к нашему обработчику вопросов
670 if tool_name == "AskUserQuestion":
671 return await handle_ask_user_question(input_data)
672 # Автоматически одобрите другие инструменты для этого примера
673 return PermissionResultAllow(updated_input=input_data)
674
675
676 async def prompt_stream():
677 yield {
678 "type": "user",
679 "message": {
680 "role": "user",
681 "content": "Help me decide on the tech stack for a new mobile app",
682 },
683 }
684
685
686 # Требуемый обходной путь: фиктивный hook держит поток открытым для can_use_tool
687 async def dummy_hook(input_data, tool_use_id, context):
688 return {"continue_": True}
689
690
691 async def main():
692 async for message in query(
693 prompt=prompt_stream(),
694 options=ClaudeAgentOptions(
695 can_use_tool=can_use_tool,
696 hooks={"PreToolUse": [HookMatcher(matcher=None, hooks=[dummy_hook])]},
697 ),
698 ):
699 if isinstance(message, ResultMessage) and message.subtype == "success":
700 print(message.result)
701
702
703 asyncio.run(main())
704 ```
705
706 ```typescript TypeScript theme={null}
707 import { query } from "@anthropic-ai/claude-agent-sdk";
708 import * as readline from "readline/promises";
709
710 // Вспомогательная функция для запроса ввода пользователя в терминале
711 async function prompt(question: string): Promise<string> {
712 const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
713 const answer = await rl.question(question);
714 rl.close();
715 return answer;
716 }
717
718 // Разберите пользовательский ввод как номер(а) варианта или свободный текст
719 function parseResponse(response: string, options: any[]): string {
720 const indices = response.split(",").map((s) => parseInt(s.trim()) - 1);
721 const labels = indices
722 .filter((i) => !isNaN(i) && i >= 0 && i < options.length)
723 .map((i) => options[i].label);
724 return labels.length > 0 ? labels.join(", ") : response;
725 }
726
727 // Отобразите вопросы Claude и соберите ответы пользователя
728 async function handleAskUserQuestion(input: any) {
729 const answers: Record<string, string> = {};
730
731 for (const q of input.questions) {
732 console.log(`\n${q.header}: ${q.question}`);
733
734 const options = q.options;
735 options.forEach((opt: any, i: number) => {
736 console.log(` ${i + 1}. ${opt.label} - ${opt.description}`);
737 });
738 if (q.multiSelect) {
739 console.log(" (Enter numbers separated by commas, or type your own answer)");
740 } else {
741 console.log(" (Enter a number, or type your own answer)");
742 }
743
744 const response = (await prompt("Your choice: ")).trim();
745 answers[q.question] = parseResponse(response, options);
746 }
747
748 // Верните ответы Claude (должны включать исходные вопросы)
749 return {
750 behavior: "allow",
751 updatedInput: { questions: input.questions, answers }
752 };
753 }
754
755 async function main() {
756 for await (const message of query({
757 prompt: "Help me decide on the tech stack for a new mobile app",
758 options: {
759 canUseTool: async (toolName, input) => {
760 // Маршрутизируйте AskUserQuestion к нашему обработчику вопросов
761 if (toolName === "AskUserQuestion") {
762 return handleAskUserQuestion(input);
763 }
764 // Автоматически одобрите другие инструменты для этого примера
765 return { behavior: "allow", updatedInput: input };
766 }
767 }
768 })) {
769 if ("result" in message) console.log(message.result);
770 }
771 }
772
773 main();
774 ```
775</CodeGroup>
776
777## Ограничения
778
779* **Подагенты**: `AskUserQuestion` в настоящее время недоступен в подагентах, порождённых через инструмент Agent
780* **Ограничения вопросов**: каждый вызов `AskUserQuestion` поддерживает 1-4 вопроса с 2-4 вариантами каждый
781
782## Другие способы получить пользовательский ввод
783
784Callback `canUseTool` и инструмент `AskUserQuestion` охватывают большинство сценариев одобрения и уточнения, но SDK предлагает другие способы получить ввод от пользователей:
785
786### Потоковый ввод
787
788Используйте [потоковый ввод](/ru/agent-sdk/streaming-vs-single-mode) когда вам нужно:
789
790* **Прервать агента в середине задачи**: отправить сигнал отмены или изменить направление, пока Claude работает
791* **Предоставить дополнительный контекст**: добавить информацию, которая нужна Claude, без ожидания, пока он спросит
792* **Создать интерфейсы чата**: позволить пользователям отправлять последующие сообщения во время долгоживущих операций
793
794Потоковый ввод идеален для разговорных пользовательских интерфейсов, где пользователи взаимодействуют с агентом на протяжении всего выполнения, а не только в контрольных точках одобрения.
795
796### Пользовательские инструменты
797
798Используйте [пользовательские инструменты](/ru/agent-sdk/custom-tools) когда вам нужно:
799
800* **Собрать структурированный ввод**: создать формы, мастера или многошаговые рабочие процессы, которые выходят за рамки формата множественного выбора `AskUserQuestion`
801* **Интегрировать внешние системы одобрения**: подключиться к существующим системам тикетов, рабочих процессов или одобрения
802* **Реализовать взаимодействия, специфичные для домена**: создать инструменты, адаптированные к потребностям вашего приложения, такие как интерфейсы проверки кода или контрольные списки развёртывания
803
804Пользовательские инструменты дают вам полный контроль над взаимодействием, но требуют больше работы по реализации, чем использование встроенного callback `canUseTool`.
805
806## Связанные ресурсы
807
808* [Настройка разрешений](/ru/agent-sdk/permissions): установите режимы и правила разрешений
809* [Управление выполнением с помощью hooks](/ru/agent-sdk/hooks): запустите пользовательский код в ключевых точках жизненного цикла агента
810* [Справочник TypeScript SDK](/ru/agent-sdk/typescript#canusetool): полная документация API canUseTool