תיעוד 134
טיפול באישורים ובקלט משתמש
הצגת בקשות אישור ושאלות הבהרה של Claude למשתמשים, והחזרת ההחלטות שלהם ל-SDK.
בזמן עבודה על משימה, לפעמים Claude צריך לבדוק דברים מול המשתמשים. ייתכן שהוא יצטרך הרשאה לפני מחיקת קבצים, או יצטרך לשאול באיזה מסד נתונים להשתמש עבור פרויקט חדש. היישום שלכם צריך להציג את הבקשות האלה למשתמשים כדי ש-Claude יוכל להמשיך עם הקלט שלהם.
Claude מבקש קלט משתמש בשני מצבים: כאשר הוא זקוק להרשאה להשתמש בכלי (כמו מחיקת קבצים או הרצת פקודות), וכאשר יש לו שאלות הבהרה (באמצעות הכלי AskUserQuestion). שני המצבים מפעילים את ה-callback שלכם canUseTool, שמשהה את הביצוע עד שתחזירו תשובה. זה שונה מסבבי שיחה רגילים שבהם Claude מסיים וממתין להודעה הבאה שלכם.
עבור שאלות הבהרה, Claude מייצר את השאלות ואת האפשרויות. התפקיד שלכם הוא להציג אותן למשתמשים ולהחזיר את הבחירות שלהם. אינכם יכולים להוסיף שאלות משלכם לתהליך הזה. אם אתם צריכים לשאול את המשתמשים משהו בעצמכם, עשו זאת בנפרד בלוגיקת היישום שלכם.
ה-callback יכול להישאר בהמתנה ללא הגבלת זמן. הביצוע נשאר מושהה עד שה-callback שלכם מחזיר ערך, וה-SDK מבטל את ההמתנה רק כאשר השאילתה עצמה מבוטלת. אם למשתמש עשוי לקחת יותר זמן להגיב ממה שהתהליך שלכם יכול להישאר פועל באופן סביר, רשמו hook מסוג PreToolUse שמחזיר את ההחלטה defer במקום להמתין ב-callback, כדי שהתהליך יוכל להסתיים ולהתחדש מאוחר יותר מתוך ההפעלה שנשמרה.
מדריך זה מראה לכם כיצד לזהות כל סוג של בקשה ולהגיב בהתאם.
#זיהוי מתי Claude זקוק לקלט
העבירו callback מסוג canUseTool באפשרויות השאילתה שלכם. ה-callback מופעל בכל פעם ש-Claude זקוק לקלט משתמש, ומקבל את שם הכלי ואת הקלט כארגומנטים:
Python:
from claude_agent_sdk import ClaudeAgentOptions
async def handle_tool_request(tool_name, input_data, context):
# Prompt user and return allow or deny
...
options = ClaudeAgentOptions(can_use_tool=handle_tool_request)TypeScript:
async function handleToolRequest(toolName, input, options) {
// options includes { signal: AbortSignal, suggestions?: PermissionUpdate[] }
// Prompt user and return allow or deny
}
const options = { canUseTool: handleToolRequest };ה-callback מופעל בשני מקרים:
- כלי זקוק לאישור: Claude רוצה להשתמש בכלי שאינו מאושר אוטומטית על ידי כלל הרשאה או מצב הרשאה. בדקו את
tool_nameעבור הכלי (למשל,"Bash","Write"). - Claude שואל שאלה: Claude קורא לכלי
AskUserQuestion. בדקו אםtool_name == "AskUserQuestion"כדי לטפל בו באופן שונה. אם אתם מציינים מערךtools, כללו אתAskUserQuestionכדי שזה יעבוד. ראו טיפול בשאלות הבהרה לפרטים.
[!WARNING] ה-callback לעולם אינו מופעל עבור כלים המאושרים אוטומטית. כל אישור מוקדם יותר בתהליך הערכת ההרשאות, כלל הרשאה מסוג allow או מצב כמו
acceptEditsאוbypassPermissions, פותר את הקריאה לפני שמתייעצים עםcanUseTool. אם אתם מציינים כלי ישירות ב-allowed_tools, בדיקתcanUseToolעבור אותו כלי תרוץ רק כאשר תהליך ההערכה מנתב את הקריאה בחזרה לבקשת אישור, כגון כלל ask או מצבplan. עבור לוגיקה שחייבת לחול על כל קריאת כלי, השתמשו בhook מסוגPreToolUse, אשר מתבצע לפני שאר התהליך ויכול לאשר, לדחות או לשנות בקשות.כלל allow אינו מאשר מראש את הפעולות שאף מצב אינו מאשר אוטומטית. ראו כיצד מוערכות הרשאות כדי לבדוק אילו מהן מגיעות ל-callback ומה קורה במצב
dontAskובמצבauto.
ניתן גם להשתמש בhook מסוג PermissionRequest כדי לשלוח התראות חיצוניות (Slack, דוא"ל, push) כאשר Claude ממתין לאישור.
#טיפול בבקשות אישור לכלים
לאחר שהעברתם callback מסוג canUseTool באפשרויות השאילתה שלכם, הוא מופעל כאשר Claude רוצה להשתמש בכלי ששום שלב מוקדם יותר בתהליך ההרשאות לא אישר. בתצורות מסוימות, כגון מצב dontAsk, הכלי Claude Code אינו קורא לו. השלב האחרון בכיצד מוערכות הרשאות מפרט אותן ומציין מה קורה לקריאה במקום זאת.
ה-callback שלכם מקבל שלושה ארגומנטים:
| ארגומנט | תיאור |
|---|---|
toolName | שם הכלי ש-Claude רוצה להשתמש בו (למשל, "Bash", "Write", "Edit") |
input | הפרמטרים ש-Claude מעביר לכלי. התוכן משתנה בהתאם לכלי. |
options (ב-TS) / context (ב-Python) | הקשר נוסף הכולל suggestions אופציונליים (רשומות PermissionUpdate מוצעות כדי למנוע בקשת אישור חוזרת) ואות ביטול. ב-TypeScript, הארגומנט signal הוא AbortSignal, ב-Python שדה ה-signal שמור לשימוש עתידי. ראו ToolPermissionContext עבור Python. |
האובייקט input מכיל פרמטרים ספציפיים לכלי. דוגמאות נפוצות:
| כלי | שדות קלט |
|---|---|
Bash | command, description, timeout |
Write | file_path, content |
Edit | file_path, old_string, new_string |
Read | file_path, offset, limit |
ראו את המדריך ל-SDK עבור סכמות קלט מלאות: Python | TypeScript.
באפשרותכם להציג מידע זה למשתמש כדי שיוכל להחליט אם לאשר או לדחות את הפעולה, ולאחר מכן להחזיר את התגובה המתאימה.
הדוגמה הבאה מבקשת מ-Claude ליצור ולמחוק קובץ בדיקה. כאשר Claude מנסה לבצע כל פעולה, ה-callback מדפיס את בקשת הכלי לטרמינל ומבקש אישור y/n.
Python:
import asyncio
from claude_agent_sdk import ClaudeAgentOptions, ResultMessage, query
from claude_agent_sdk.types import (
HookMatcher,
PermissionResultAllow,
PermissionResultDeny,
ToolPermissionContext,
)
async def can_use_tool(
tool_name: str, input_data: dict, context: ToolPermissionContext
) -> PermissionResultAllow | PermissionResultDeny:
# Display the tool request
print(f"\nTool: {tool_name}")
if tool_name == "Bash":
print(f"Command: {input_data.get('command')}")
if input_data.get("description"):
print(f"Description: {input_data.get('description')}")
else:
print(f"Input: {input_data}")
# Get user approval
response = input("Allow this action? (y/n): ")
# Return allow or deny based on user's response
if response.lower() == "y":
# Allow: tool executes with the original (or modified) input
return PermissionResultAllow(updated_input=input_data)
else:
# Deny: tool doesn't execute, Claude sees the message
return PermissionResultDeny(message="User denied this action")
# Required workaround: dummy hook keeps the stream open for can_use_tool
async def dummy_hook(input_data, tool_use_id, context):
return {"continue_": True}
async def prompt_stream():
yield {
"type": "user",
"message": {
"role": "user",
"content": "Create a test file in /tmp and then delete it",
},
}
async def main():
async for message in query(
prompt=prompt_stream(),
options=ClaudeAgentOptions(
can_use_tool=can_use_tool,
hooks={"PreToolUse": [HookMatcher(matcher=None, hooks=[dummy_hook])]},
),
):
if isinstance(message, ResultMessage) and message.subtype == "success":
print(message.result)
asyncio.run(main())TypeScript:
import { query } from "@anthropic-ai/claude-agent-sdk";
import * as readline from "readline";
// Helper to prompt user for input in the terminal
function prompt(question: string): Promise<string> {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
return new Promise((resolve) =>
rl.question(question, (answer) => {
rl.close();
resolve(answer);
})
);
}
for await (const message of query({
prompt: "Create a test file in /tmp and then delete it",
options: {
canUseTool: async (toolName, input) => {
// Display the tool request
console.log(`\nTool: ${toolName}`);
if (toolName === "Bash") {
console.log(`Command: ${input.command}`);
if (input.description) console.log(`Description: ${input.description}`);
} else {
console.log(`Input: ${JSON.stringify(input, null, 2)}`);
}
// Get user approval
const response = await prompt("Allow this action? (y/n): ");
// Return allow or deny based on user's response
if (response.toLowerCase() === "y") {
// Allow: tool executes with the original (or modified) input
return { behavior: "allow", updatedInput: input };
} else {
// Deny: tool doesn't execute, Claude sees the message
return { behavior: "deny", message: "User denied this action" };
}
}
}
})) {
if ("result" in message) console.log(message.result);
}[!NOTE] ב-Python, ה-callback מסוג
can_use_toolדורש מצב הזרמה (streaming mode). כאשר אתם מעבירים זרם הודעות סופי דרךquery(prompt=generator)אוClaudeSDKClient.connect(prompt=async_iterable), ה-SDK סוגר את זרם הקלט לאחר ההודעה האחרונה, לפני שניתן להפעיל את callback ההרשאות, אלא אם כן hook רשום או שרת MCP הפועל בתוך התהליך משאירים אותו פתוח. הדוגמה שלמעלה משאירה אותו פתוח באמצעות hook מסוגPreToolUseשמחזיר{"continue_": True}. התחברות ללא prompt ושליחת הודעות דרךClaudeSDKClient.query()משאירה את הזרם פתוח בפני עצמה ואינה דורשת hook.
דוגמה זו משתמשת בתהליך y/n שבו כל קלט שאינו y נחשב כדחייה. בפועל, ייתכן שתרצו לבנות ממשק משתמש עשיר יותר המאפשר למשתמשים לשנות את הבקשה, לספק משוב, או לנתב מחדש את Claude לחלוטין. ראו תגובה לבקשות כלי עבור כל הדרכים שבהן ניתן להגיב.
#תגובה לבקשות כלי
ה-callback שלכם מחזיר אחד משני סוגי תגובה:
| תגובה | Python | TypeScript |
|---|---|---|
| אישור (Allow) | PermissionResultAllow(updated_input=...) | { behavior: "allow", updatedInput } |
| דחייה (Deny) | PermissionResultDeny(message=...) | { behavior: "deny", message } |
בעת אישור, הכלי רץ עם הקלט ש-Claude ביקש, אלא אם כן אתם מחזירים קלט מעודכן, updatedInput ב-TypeScript או updated_input ב-Python. לפני גרסה 2.1.207, Claude Code דחה תוצאת אישור שהשמיטה את updatedInput ודחה את קריאת הכלי עם שגיאת אימות.
בעת דחייה, ספקו הודעה המסבירה מדוע. Claude רואה הודעה זו ועשוי להתאים את גישתו.
מעבר לאישור או דחייה, באפשרותכם לשנות את הקלט של הכלי או לספק הקשר שעוזר ל-Claude להתאים את גישתו:
- אישור: מתן אפשרות לכלי להתבצע כפי ש-Claude ביקש
- אישור עם שינויים: שינוי הקלט לפני הביצוע (למשל, ניקוי נתיבים, הוספת אילוצים)
- אישור וזכירה: החזרת כלל הרשאה מוצע בחזרה כך שקריאות תואמות ידלגו על הבקשה בפעם הבאה
- דחייה: חסימת הכלי והסבר ל-Claude מדוע
- הצעת חלופה: חסימה אך הכוונת Claude לעבר מה שהמשתמש רוצה במקום זאת
- ניתוב מחדש לחלוטין: שימוש בקלט מוזרם כדי לשלוח ל-Claude הוראה חדשה לחלוטין
פונקציות העזר ask_user ו-askUser בקטעי הקוד הבאים מייצגות את ממשק בקשת הקלט של היישום שלכם.
#אישור (Approve)
המשתמש מאשר את הפעולה כפי שהיא. העבירו את ה-input מה-callback שלכם ללא שינוי והכלי יתבצע בדיוק כפי ש-Claude ביקש.
Python:
async def can_use_tool(tool_name, input_data, context):
print(f"Claude wants to use {tool_name}")
approved = await ask_user("Allow this action?")
if approved:
return PermissionResultAllow(updated_input=input_data)
return PermissionResultDeny(message="User declined")TypeScript:
canUseTool: async (toolName, input) => {
console.log(`Claude wants to use ${toolName}`);
const approved = await askUser("Allow this action?");
if (approved) {
return { behavior: "allow", updatedInput: input };
}
return { behavior: "deny", message: "User declined" };
};#אישור עם שינויים (Approve with changes)
המשתמש מאשר אך רוצה לשנות את הבקשה תחילה. באפשרותכם לשנות את הקלט לפני שהכלי מתבצע. Claude רואה את התוצאה אך לא נאמר לו ששיניתם דבר. שימושי לניקוי פרמטרים, הוספת אילוצים או הגבלת היקף הגישה.
Python:
async def can_use_tool(tool_name, input_data, context):
if tool_name == "Bash":
# User approved, but scope all commands to sandbox
sandboxed_input = {**input_data}
sandboxed_input["command"] = input_data["command"].replace(
"/tmp", "/tmp/sandbox"
)
return PermissionResultAllow(updated_input=sandboxed_input)
return PermissionResultAllow(updated_input=input_data)TypeScript:
canUseTool: async (toolName, input) => {
if (toolName === "Bash") {
// User approved, but scope all commands to sandbox
const sandboxedInput = {
...input,
command: input.command.replace("/tmp", "/tmp/sandbox")
};
return { behavior: "allow", updatedInput: sandboxedInput };
}
return { behavior: "allow", updatedInput: input };
};#אישור וזכירה (Approve and remember)
המשתמש מאשר ואינו רוצה להישאל שוב עבור סוג כזה של קריאה. הארגומנט השלישי של ה-callback מעביר את suggestions, מערך של רשומות PermissionUpdate מוכנות לשימוש. החזירו אחת מהן ב-updatedPermissions כדי להחיל אותה. הצעה עם היעד localSettings כותבת את הכלל אל .claude/settings.local.json כך שהפעלות עתידיות ידלגו על בקשת האישור עבור קריאות תואמות.
דוגמת ה-Python דורשת את claude-agent-sdk בגרסה 0.1.80 ואילך.
Python:
async def can_use_tool(tool_name, input_data, context):
choice = await ask_user(f"Allow {tool_name}?", ["once", "always", "no"])
if choice == "always":
persist = [
s for s in context.suggestions if s.destination == "localSettings"
]
return PermissionResultAllow(
updated_input=input_data, updated_permissions=persist
)
if choice == "once":
return PermissionResultAllow(updated_input=input_data)
return PermissionResultDeny(message="User declined")TypeScript:
canUseTool: async (toolName, input, { suggestions = [] }) => {
const choice = await askUser(`Allow ${toolName}?`, ["once", "always", "no"]);
if (choice === "always") {
const persist = suggestions.filter(
(s) => s.destination === "localSettings"
);
return {
behavior: "allow",
updatedInput: input,
updatedPermissions: persist
};
}
if (choice === "once") {
return { behavior: "allow", updatedInput: input };
}
return { behavior: "deny", message: "User declined" };
};#דחייה (Reject)
המשתמש אינו רוצה שפעולה זו תתרחש. חסמו את הכלי וספקו הודעה המסבירה מדוע. Claude רואה הודעה זו ועשוי לנסות גישה שונה.
Python:
async def can_use_tool(tool_name, input_data, context):
approved = await ask_user(f"Allow {tool_name}?")
if not approved:
return PermissionResultDeny(message="User rejected this action")
return PermissionResultAllow(updated_input=input_data)TypeScript:
canUseTool: async (toolName, input) => {
const approved = await askUser(`Allow ${toolName}?`);
if (!approved) {
return {
behavior: "deny",
message: "User rejected this action"
};
}
return { behavior: "allow", updatedInput: input };
};#הצעת חלופה (Suggest alternative)
המשתמש אינו רוצה בפעולה הספציפית הזו, אך יש לו רעיון אחר. חסמו את הכלי וכללו הנחיות בהודעה שלכם. Claude יקרא זאת ויחליט כיצד להמשיך על סמך המשוב שלכם.
Python:
async def can_use_tool(tool_name, input_data, context):
if tool_name == "Bash" and "rm" in input_data.get("command", ""):
# User doesn't want to delete, suggest archiving instead
return PermissionResultDeny(
message="User doesn't want to delete files. They asked if you could compress them into an archive instead."
)
return PermissionResultAllow(updated_input=input_data)TypeScript:
canUseTool: async (toolName, input) => {
if (toolName === "Bash" && input.command.includes("rm")) {
// User doesn't want to delete, suggest archiving instead
return {
behavior: "deny",
message:
"User doesn't want to delete files. They asked if you could compress them into an archive instead."
};
}
return { behavior: "allow", updatedInput: input };
};#ניתוב מחדש לחלוטין (Redirect entirely)
עבור שינוי כיוון מוחלט (לא רק הכוונה קלה), השתמשו בקלט מוזרם כדי לשלוח ל-Claude הוראה חדשה ישירות. הדבר עוקף את בקשת הכלי הנוכחית ומעניק ל-Claude הוראות חדשות לחלוטין לפעול לפיהן.
#טיפול בשאלות הבהרה
כאשר Claude זקוק להכוונה נוספת במשימה שיש לה מספר גישות תקפות, הוא קורא לכלי AskUserQuestion. הדבר מפעיל את ה-callback שלכם canUseTool כאשר toolName מוגדר כ-AskUserQuestion. הקלט מכיל את השאלות של Claude כאפשרויות בחירה מרובה, שאתם מציגים למשתמש ומחזירים את הבחירות שלו.
[!TIP] שאלות הבהרה נפוצות במיוחד במצב
plan, שבו Claude חוקר את בסיס הקוד ושואל שאלות לפני שהוא מציע תוכנית. הדבר הופך את מצב plan לאידיאלי עבור תהליכי עבודה אינטראקטיביים שבהם אתם רוצים ש-Claude יאסוף דרישות לפני ביצוע שינויים.
השלבים הבאים מראים כיצד לטפל בשאלות הבהרה:
- העברת callback מסוג canUseTool:
העבירו callback מסוג
canUseToolבאפשרויות השאילתה שלכם. כברירת מחדל,AskUserQuestionזמין. אם אתם מציינים מערךtoolsכדי להגביל את יכולותיו של Claude (לדוגמה, סוכן לקריאה בלבד עםRead,Globו-Grepבלבד), כללו אתAskUserQuestionבמערך זה. אחרת, Claude לא יוכל לשאול שאלות הבהרה:
Python:
async for message in query(
prompt="Analyze this codebase",
options=ClaudeAgentOptions(
# Include AskUserQuestion in your tools list
tools=["Read", "Glob", "Grep", "AskUserQuestion"],
can_use_tool=can_use_tool,
),
):
print(message)TypeScript:
for await (const message of query({
prompt: "Analyze this codebase",
options: {
// Include AskUserQuestion in your tools list
tools: ["Read", "Glob", "Grep", "AskUserQuestion"],
canUseTool: async (toolName, input) => {
// Handle clarifying questions here
}
}
})) {
console.log(message);
}- זיהוי AskUserQuestion:
ב-callback שלכם, בדקו אם
toolNameשווה ל-AskUserQuestionכדי לטפל בו באופן שונה מכלים אחרים:
Python:
async def can_use_tool(tool_name: str, input_data: dict, context):
if tool_name == "AskUserQuestion":
# Your implementation to collect answers from the user
return await handle_clarifying_questions(input_data)
# Handle other tools normally
return await prompt_for_approval(tool_name, input_data)TypeScript:
canUseTool: async (toolName, input) => {
if (toolName === "AskUserQuestion") {
// Your implementation to collect answers from the user
return handleClarifyingQuestions(input);
}
// Handle other tools normally
return promptForApproval(toolName, input);
};- ניתוח קלט השאלה:
הקלט מכיל את השאלות של Claude במערך
questions. לכל שאלה ישquestion(הטקסט להצגה),header(כותרת קצרה),options(האפשרויות לבחירה), ו-multiSelect(האם מותר לבחור מספר אפשרויות):
{
"questions": [
{
"question": "How should I format the output?",
"header": "Format",
"options": [
{ "label": "Summary", "description": "Brief overview" },
{ "label": "Detailed", "description": "Full explanation" }
],
"multiSelect": false
},
{
"question": "Which sections should I include?",
"header": "Sections",
"options": [
{ "label": "Introduction", "description": "Opening context" },
{ "label": "Conclusion", "description": "Final summary" }
],
"multiSelect": true
}
]
}ראו פורמט השאלות עבור תיאורי שדות מלאים.
איסוף תשובות מהמשתמש: הציגו את השאלות למשתמש ואספו את הבחירות שלו. האופן שבו תעשו זאת תלוי ביישום שלכם: בקשת קלט בטרמינל, טופס אינטרנט, תיבת דו שיח בנייד, וכדומה.
החזרת תשובות ל-Claude: בנו את האובייקט
answersכמבנה נתונים שבו כל מפתח הוא טקסט ה-questionוכל ערך הוא ה-labelשל האפשרות שנבחרה:
| מתוך אובייקט השאלה | שימוש בתור |
|---|---|
שדה question (למשל, "How should I format the output?") | מפתח (Key) |
שדה label של האפשרות שנבחרה (למשל, "Summary") | ערך (Value) |
עבור שאלות עם בחירה מרובה, העבירו מערך של תוויות או חברו אותן באמצעות ", ". אם אתם תומכים בהזנת טקסט חופשי, השתמשו בטקסט המותאם אישית של המשתמש בתור הערך.
Python:
return PermissionResultAllow(
updated_input={
"questions": input_data.get("questions", []),
"answers": {
"How should I format the output?": "Summary",
"Which sections should I include?": ["Introduction", "Conclusion"],
},
}
)TypeScript:
return {
behavior: "allow",
updatedInput: {
questions: input.questions,
answers: {
"How should I format the output?": "Summary",
"Which sections should I include?": "Introduction, Conclusion"
}
}
};#פורמט השאלות
הקלט מכיל את השאלות ש-Claude יצר במערך questions. לכל שאלה יש את השדות הבאים:
| שדה | תיאור |
|---|---|
question | טקסט השאלה המלא להצגה |
header | תווית קצרה לשאלה (עד 12 תווים) |
options | מערך של 2 עד 4 אפשרויות, כל אחת עם label ו-description. ב-TypeScript: אופציונלית preview (ראו להלן) |
multiSelect | אם true, משתמשים יכולים לבחור מספר אפשרויות |
המבנה שה-callback שלכם מקבל:
{
"questions": [
{
"question": "How should I format the output?",
"header": "Format",
"options": [
{ "label": "Summary", "description": "Brief overview of key points" },
{ "label": "Detailed", "description": "Full explanation with examples" }
],
"multiSelect": false
}
]
}#תצוגות מקדימות של אפשרויות (TypeScript)
השדה toolConfig.askUserQuestion.previewFormat מוסיף שדה preview לכל אפשרות, כך שהיישום שלכם יוכל להציג הדמיה חזותית לצד התווית. ללא הגדרה זו, Claude אינו מייצר תצוגות מקדימות והשדה אינו קיים.
previewFormat | מה שדה preview מכיל |
|---|---|
| לא מוגדר (ברירת מחדל) | השדה אינו קיים. Claude אינו מייצר תצוגות מקדימות. |
"markdown" | אמנות ASCII (ASCII art) ובלוקי קוד תחומים |
"html" | מקטע <div> מעוצב (ה-SDK פוסל תגיות <script>, <style> ו-<!DOCTYPE> לפני שה-callback שלכם רץ) |
הפורמט חל על כל השאלות בהפעלה. Claude כולל preview באפשרויות שבהן השוואה חזותית מועילה (בחירות פריסה, ערכות צבעים) ומשמיט אותו במקרים שבהם היא אינה נחוצה (אישורי כן/לא, בחירות טקסט בלבד). בדקו אם הערך הוא undefined לפני הרינדור.
import { query } from "@anthropic-ai/claude-agent-sdk";
for await (const message of query({
prompt: "Help me choose a card layout",
options: {
toolConfig: {
askUserQuestion: { previewFormat: "html" }
},
canUseTool: async (toolName, input) => {
// input.questions[].options[].preview is an HTML string or undefined
return { behavior: "allow", updatedInput: input };
}
}
})) {
// ...
}אפשרות עם תצוגה מקדימה של HTML:
{
"label": "Compact",
"description": "Title and metric value only",
"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>"
}#פורמט התגובה
החזירו אובייקט answers הממפה את שדה question של כל שאלה ל-label של האפשרות שנבחרה:
| שדה | תיאור |
|---|---|
questions | העבירו את מערך השאלות המקורי (נדרש לצורך עיבוד הכלי) |
answers | אובייקט שבו המפתחות הם טקסט השאלה והערכים הם התוויות שנבחרו |
response | מענה אופציונלי בטקסט חופשי שהמשתמש הקליד במקום לענות על השאלות המובנות |
עבור שאלות עם בחירה מרובה, העבירו מערך של תוויות או חברו אותן באמצעות ", ". עבור טקסט חופשי לכל שאלה, כגון אפשרות "Other", הציבו את טקסט המשתמש בתוך answers[question] כפי שמוצג בסעיף תמיכה בהזנת טקסט חופשי. הגדירו את response רק כאשר ממשק המשתמש שלכם מאפשר למשתמש לסגור את כרטיס השאלה ולהקליד מענה כללי שאינו תשובה לשאלה ספציפית כלשהי. כאשר response מוגדר, Claude מקבל "The user responded: …" במקום את רשימת התשובות לכל שאלה.
{
"questions": [
// ...
],
"answers": {
"How should I format the output?": "Summary",
"Which sections should I include?": ["Introduction", "Conclusion"]
}
}#תמיכה בהזנת טקסט חופשי
האפשרויות המוגדרות מראש של Claude לא תמיד יכסו את מה שהמשתמשים רוצים. כדי לאפשר למשתמשים להקליד תשובה משלהם:
- הציגו בחירה נוספת של "Other" לאחר האפשרויות של Claude, המקבלת קלט טקסט
- השתמשו בטקסט המותאם אישית של המשתמש כערך התשובה (ולא במילה "Other")
ראו את הדוגמה המלאה להלן למימוש מלא.
#דוגמה מלאה
Claude שואל שאלות הבהרה כאשר הוא זקוק לקלט משתמש כדי להמשיך. לדוגמה, כאשר מבקשים ממנו לעזור בהחלטה על טכנולוגיות עבור אפליקציה סלולרית, Claude עשוי לשאול לגבי פיתוח cross-platform לעומת native, העדפות צד שרת, או פלטפורמות יעד. שאלות אלו מסייעות ל-Claude לקבל החלטות שתואמות את העדפות המשתמש במקום לנחש.
דוגמה זו מטפלת בשאלות אלו ביישום טרמינל. הנה מה שקורה בכל שלב:
- ניתוב הבקשה: ה-callback מסוג
canUseToolבודק אם שם הכלי הוא"AskUserQuestion"ומנתב למטפל ייעודי - הצגת השאלות: המטפל עובר בלולאה על מערך
questionsומדפיס כל שאלה עם אפשרויות ממוספרות - איסוף קלט: המשתמש יכול להזין מספר כדי לבחור אפשרות, או להקליד טקסט חופשי ישירות (למשל, "jquery", "i don't know")
- מיפוי תשובות: הקוד בודק אם הקלט הוא מספרי (משתמש בתווית של האפשרות) או טקסט חופשי (משתמש בטקסט ישירות)
- החזרה ל-Claude: התגובה כוללת הן את מערך
questionsהמקורי והן את מיפויanswers
שמרו את גרסת ה-TypeScript בשם ask.ts והריצו אותה באמצעות npx tsx ask.ts, או שמרו את גרסת ה-Python בשם ask.py והריצו אותה באמצעות python ask.py.
Python:
import asyncio
from claude_agent_sdk import ClaudeAgentOptions, ResultMessage, query
from claude_agent_sdk.types import HookMatcher, PermissionResultAllow
def parse_response(response: str, options: list) -> str:
"""Parse user input as option number(s) or free text."""
try:
indices = [int(s.strip()) - 1 for s in response.split(",")]
labels = [options[i]["label"] for i in indices if 0 <= i < len(options)]
return ", ".join(labels) if labels else response
except ValueError:
return response
async def handle_ask_user_question(input_data: dict) -> PermissionResultAllow:
"""Display Claude's questions and collect user answers."""
answers = {}
for q in input_data.get("questions", []):
print(f"\n{q['header']}: {q['question']}")
options = q["options"]
for i, opt in enumerate(options):
print(f" {i + 1}. {opt['label']} - {opt['description']}")
if q.get("multiSelect"):
print(" (Enter numbers separated by commas, or type your own answer)")
else:
print(" (Enter a number, or type your own answer)")
response = input("Your choice: ").strip()
answers[q["question"]] = parse_response(response, options)
return PermissionResultAllow(
updated_input={
"questions": input_data.get("questions", []),
"answers": answers,
}
)
async def can_use_tool(
tool_name: str, input_data: dict, context
) -> PermissionResultAllow:
# Route AskUserQuestion to our question handler
if tool_name == "AskUserQuestion":
return await handle_ask_user_question(input_data)
# Auto-approve other tools for this example
return PermissionResultAllow(updated_input=input_data)
async def prompt_stream():
yield {
"type": "user",
"message": {
"role": "user",
"content": "Help me decide on the tech stack for a new mobile app",
},
}
# Required workaround: dummy hook keeps the stream open for can_use_tool
async def dummy_hook(input_data, tool_use_id, context):
return {"continue_": True}
async def main():
async for message in query(
prompt=prompt_stream(),
options=ClaudeAgentOptions(
can_use_tool=can_use_tool,
hooks={"PreToolUse": [HookMatcher(matcher=None, hooks=[dummy_hook])]},
),
):
if isinstance(message, ResultMessage) and message.subtype == "success":
print(message.result)
asyncio.run(main())TypeScript:
import { query } from "@anthropic-ai/claude-agent-sdk";
import * as readline from "readline/promises";
// Helper to prompt user for input in the terminal
async function prompt(question: string): Promise<string> {
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
const answer = await rl.question(question);
rl.close();
return answer;
}
// Parse user input as option number(s) or free text
function parseResponse(response: string, options: any[]): string {
const indices = response.split(",").map((s) => parseInt(s.trim()) - 1);
const labels = indices
.filter((i) => !isNaN(i) && i >= 0 && i < options.length)
.map((i) => options[i].label);
return labels.length > 0 ? labels.join(", ") : response;
}
// Display Claude's questions and collect user answers
async function handleAskUserQuestion(input: any) {
const answers: Record<string, string> = {};
for (const q of input.questions) {
console.log(`\n${q.header}: ${q.question}`);
const options = q.options;
options.forEach((opt: any, i: number) => {
console.log(` ${i + 1}. ${opt.label} - ${opt.description}`);
});
if (q.multiSelect) {
console.log(" (Enter numbers separated by commas, or type your own answer)");
} else {
console.log(" (Enter a number, or type your own answer)");
}
const response = (await prompt("Your choice: ")).trim();
answers[q.question] = parseResponse(response, options);
}
// Return the answers to Claude (must include original questions)
return {
behavior: "allow",
updatedInput: { questions: input.questions, answers }
};
}
async function main() {
for await (const message of query({
prompt: "Help me decide on the tech stack for a new mobile app",
options: {
canUseTool: async (toolName, input) => {
// Route AskUserQuestion to our question handler
if (toolName === "AskUserQuestion") {
return handleAskUserQuestion(input);
}
// Auto-approve other tools for this example
return { behavior: "allow", updatedInput: input };
}
}
})) {
if ("result" in message) console.log(message.result);
}
}
main();#מגבלות
- סוכני משנה (Subagents): הכלי
AskUserQuestionאינו זמין כרגע בסוכני משנה שנוצרו באמצעות הכלי Agent - מגבלות שאלות: כל קריאה ל-
AskUserQuestionתומכת ב-1 עד 4 שאלות, עם 2 עד 4 אפשרויות לכל אחת
#דרכים נוספות לקבלת קלט משתמש
ה-callback מסוג canUseTool והכלי AskUserQuestion מכסים את רוב תרחישי האישור וההבהרה, אך ה-SDK מציע דרכים נוספות לקבלת קלט ממשתמשים:
#קלט מוזרם (Streaming input)
השתמשו בקלט מוזרם כאשר אתם צריכים:
- להפריע לסוכן באמצע משימה: שליחת אות ביטול או שינוי כיוון בזמן ש-Claude עובד
- לספק הקשר נוסף: הוספת מידע ש-Claude זקוק לו מבלי לחכות שהוא יבקש
- לבנות ממשקי צ'אט: מתן אפשרות למשתמשים לשלוח הודעות המשך במהלך פעולות הנמשכות זמן רב
קלט מוזרם אידיאלי עבור ממשקי משתמש שיחתיים שבהם משתמשים מתקשרים עם הסוכן לאורך כל הביצוע, ולא רק בנקודות ביקורת לאישור.
#כלים מותאמים אישית (Custom tools)
השתמשו בכלים מותאמים אישית כאשר אתם צריכים:
- לאסוף קלט מובנה: בניית טפסים, אשפים, או תהליכי עבודה מרובי שלבים החורגים מפורמט הבחירה המרובה של
AskUserQuestion - לבצע אינטגרציה עם מערכות אישור חיצוניות: התחברות לפלטפורמות קיימות לניהול פניות, תהליכי עבודה או אישורים
- לממש אינטראקציות ספציפיות לתחום: יצירת כלים המותאמים לצורכי היישום שלכם, כמו ממשקי סקירת קוד או רשימות תיוג לפריסה
כלים מותאמים אישית מעניקים לכם שליטה מלאה באינטראקציה, אך דורשים עבודת מימוש רבה יותר מאשר שימוש ב-callback המובנה canUseTool.
#משאבים קשורים
- הגדרת הרשאות: הגדרת מצבי הרשאות וכללים
- בקרת ביצוע באמצעות hooks: הרצת קוד מותאם אישית בנקודות מפתח במחזור החיים של הסוכן
- תיעוד TypeScript SDK: תיעוד מלא של ה-API עבור
canUseTool