מדריך קלוד קוד בעברית

תיעוד 175

טיפול באישורים ובקלט משתמש

הצג למשתמשים את בקשות האישור ושאלות ההבהרה של קלוד, ואז החזר את ההחלטות שלהם ל-SDK.

בזמן עבודה על משימה, קלוד לפעמים צריך לפנות למשתמשים. ייתכן שהוא צריך הרשאה לפני מחיקת קבצים, או לשאול באיזה מסד נתונים להשתמש לפרויקט חדש. האפליקציה שלך צריכה להציג את הבקשות האלה למשתמשים כדי שקלוד יוכל להמשיך עם הקלט שלהם.

קלוד מבקש קלט משתמש בשני מצבים: כשהוא צריך הרשאה להשתמש בכלי (כמו מחיקת קבצים או הרצת פקודות), וכשיש לו שאלות הבהרה (דרך הכלי AskUserQuestion). שניהם מפעילים את ה-callback canUseTool שלך, שעוצר את הביצוע עד שאתה מחזיר תגובה. זה שונה מתורות שיחה רגילים שבהם קלוד מסיים ומחכה להודעה הבאה שלך.

לשאלות הבהרה, קלוד מייצר את השאלות ואת האפשרויות. התפקיד שלך הוא להציג אותן למשתמשים ולהחזיר את הבחירות שלהם. אי אפשר להוסיף שאלות משלך לזרימה הזו. אם אתה צריך לשאול את המשתמשים משהו בעצמך, עשה זאת בנפרד בלוגיקה של האפליקציה.

ה-callback יכול להישאר ממתין ללא הגבלת זמן. הביצוע נשאר מושהה עד שה-callback שלך חוזר. אם משתמש עלול לקחת יותר זמן להגיב ממה שהתהליך שלך יכול להישאר רץ באופן סביר, רשום hook מסוג PreToolUse שמחזיר את החלטת defer במקום להמתין ב-callback, כדי שהתהליך יוכל לצאת ולהתחדש מאוחר יותר מהסשן שנשמר.

המדריך הזה מראה איך לזהות כל סוג בקשה ולהגיב בהתאם.

#זיהוי מתי קלוד צריך קלט

העבר callback מסוג canUseTool באפשרויות השאילתה. ה-callback מופעל בכל פעם שקלוד צריך קלט משתמש, ומקבל את שם הכלי ואת הקלט כארגומנטים:

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)
import type { CanUseTool } from "@anthropic-ai/claude-agent-sdk";

const handleToolRequest: CanUseTool = async (toolName, input, options) => {
  // options includes { signal: AbortSignal, suggestions?: PermissionUpdate[] }
  // Prompt the user here, then return allow or deny
  return { behavior: "deny", message: "User declined" };
};

const options = { canUseTool: handleToolRequest };

ה-callback מופעל בשני מקרים:

  1. הכלי צריך אישור: קלוד רוצה להשתמש בכלי שלא אושר אוטומטית על ידי כלל הרשאה או מצב הרשאה. בדוק את tool_name עבור הכלי (לדוגמה, "Bash", "Write").
  2. קלוד שואל שאלה: קלוד קורא לכלי AskUserQuestion. בדוק אם tool_name == "AskUserQuestion" כדי לטפל בזה אחרת. אם אתה מציין מערך tools, כלול את AskUserQuestion כדי שזה יעבוד. למידע נוסף, ראה טיפול בשאלות הבהרה.

[!WARNING]

שלב קודם יכול לאשר או לדחות קריאה לפני ש-canUseTool רץ. זרימת הערכת ההרשאות בודקת כללי דחייה, את מצב ההרשאה, וכללי אישור לפני ה-callback שלך.

כדי להריץ את הבדיקות שלך על קריאות שכלל או מצב ההרשאה כבר אישרו, השתמש ב-hook מסוג PreToolUse, שרץ עבור הקריאות האלה ויכול לאשר, לדחות או לשנות אותן.

אפשר גם להשתמש ב-hook מסוג PermissionRequest כדי לשלוח התראות חיצוניות (Slack, אימייל, push) כשקלוד ממתין לאישור.

#טיפול בבקשות אישור לכלי

אחרי שהעברת callback מסוג canUseTool באפשרויות השאילתה, הוא מופעל כשקלוד רוצה להשתמש בכלי ששום דבר מוקדם יותר בזרימת ההרשאות לא אישר. בחלק מההגדרות, כמו מצב dontAsk, Claude Code לא קורא לו. השלב האחרון של איך הרשאות מוערכות מפרט אותן ואומר מה קורה לקריאה במקום זאת.

ה-callback שלך מקבל שלושה ארגומנטים:

ארגומנטתיאור
toolNameשם הכלי שקלוד רוצה להשתמש בו (לדוגמה, "Bash", "Write", "Edit")
inputהפרמטרים שקלוד מעביר לכלי. התוכן משתנה לפי הכלי.
options (TS) / context (Python)הקשר נוסף שכולל suggestions אופציונלי (רשומות PermissionUpdate מוצעות כדי להימנע משאילתה חוזרת) ואות ביטול. ב-TypeScript, signal הוא AbortSignal. ב-Python, שדה האות שמור לשימוש עתידי. ראה ToolPermissionContext עבור Python.

אובייקט input מכיל פרמטרים ייחודיים לכלי. דוגמאות נפוצות:

כלישדות קלט
Bashcommand, description, timeout
Writefile_path, content
Editfile_path, old_string, new_string
Readfile_path, offset, limit

ראה את חומר העזר של ה-SDK לסכמות קלט מלאות: Python | TypeScript.

אפשר להציג את המידע הזה למשתמש כדי שיוכל להחליט אם לאשר או לדחות את הפעולה, ואז להחזיר את התגובה המתאימה.

הדוגמה הבאה מבקשת מקלוד ליצור ולמחוק קובץ בדיקה. כשקלוד מנסה כל פעולה, ה-callback מדפיס את בקשת הכלי לטרמינל ומבקש אישור y/n.

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())
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);
}

הדוגמה הזו משתמשת בזרימת y/n שבה כל קלט שאינו y נחשב לדחייה. בפועל, אפשר לבנות ממשק עשיר יותר שמאפשר למשתמשים לשנות את הבקשה, לתת משוב, או להפנות את קלוד לגמרי. ראה תגובה לבקשות כלי לכל הדרכים שאפשר להגיב בהן.

#תגובה לבקשות כלי

ה-callback שלך מחזיר אחד משני סוגי תגובה:

תגובהPythonTypeScript
AllowPermissionResultAllow(updated_input=...){ behavior: "allow", updatedInput }
DenyPermissionResultDeny(message=...){ behavior: "deny", message }

כשמאשרים, הכלי רץ עם הקלט שקלוד ביקש, אלא אם מחזירים קלט ששונה, updatedInput ב-TypeScript או updated_input ב-Python. לפני v2.1.207, Claude Code דחה תוצאת אישור שהשמיטה את updatedInput ודחה את קריאת הכלי עם שגיאת ולידציה.

כשדוחים, ספק הודעה שמסבירה למה. קלוד רואה את ההודעה הזו ועשוי להתאים את הגישה שלו.

מעבר לאישור או דחייה, אפשר לשנות את הקלט של הכלי או לספק הקשר שעוזר לקלוד להתאים את הגישה שלו:

  • אישור: לתת לכלי לרוץ כפי שקלוד ביקש
  • אישור עם שינויים: לשנות את הקלט לפני הביצוע (לדוגמה, לנקות נתיבים, להוסיף אילוצים)
  • אישור וזכירה: להחזיר כלל הרשאה מוצע כדי שקריאות תואמות ידלגו על השאילה בפעם הבאה
  • דחייה: לחסום את הכלי ולומר לקלוד למה
  • הצעת חלופה: לחסום אבל להנחות את קלוד למה שהמשתמש רוצה במקום זאת
  • הפניה מלאה: להשתמש ב-קלט בזרימה כדי לשלוח לקלוד הוראה חדשה לגמרי

העוזרים ask_user ו-askUser בקטעים הבאים מייצגים את ממשק השאילה של האפליקציה שלך.

#אישור

המשתמש מאשר את הפעולה כפי שהיא. העבר את input מה-callback שלך ללא שינוי והכלי רץ בדיוק כפי שקלוד ביקש.

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")
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" };
};

#אישור עם שינויים

המשתמש מאשר אבל רוצה לשנות את הבקשה קודם. אפשר לשנות את הקלט לפני שהכלי רץ. קלוד רואה את התוצאה אבל לא נאמר לו ששינית משהו. שימושי לניקוי פרמטרים, הוספת אילוצים, או תיחום גישה.

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)
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 };
};

#אישור וזכירה

המשתמש מאשר ולא רוצה שישאלו אותו שוב עבור סוג הקריאה הזה. הארגומנט השלישי של ה-callback נושא suggestions, מערך של רשומות PermissionUpdate מוכנות. החזר אחת מהן ב-updatedPermissions כדי להחיל אותה. הצעה עם יעד localSettings כותבת את הכלל ל-.claude/settings.local.json כדי שסשנים עתידיים ידלגו על השאילה עבור קריאות תואמות.

ב-TypeScript, דלג על בחירת ה-always-allow לבקשה שהאפשרויות שלה נושאות suppressAlwaysAllowRule: true. הרמז דורש Agent SDK v0.3.268 או מאוחר יותר, וה-context ב-Python לא נושא אותו.

דוגמת Python דורשת claude-agent-sdk 0.1.80 או מאוחר יותר.

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")
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" };
};

#דחייה

המשתמש לא רוצה שהפעולה הזו תקרה. חסום את הכלי וספק הודעה שמסבירה למה. קלוד רואה את ההודעה הזו ועשוי לנסות גישה אחרת.

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)
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 };
};

#הצעת חלופה

המשתמש לא רוצה את הפעולה הספציפית הזו, אבל יש לו רעיון אחר. חסום את הכלי וכלול הנחיה בהודעה. קלוד יקרא את זה ויחליט איך להמשיך לפי המשוב שלך.

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)
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 };
};

#הפניה מלאה

לשינוי כיוון מלא (לא רק דחיפה קלה), השתמש ב-קלט בזרימה כדי לשלוח לקלוד הוראה חדשה ישירות. זה עוקף את בקשת הכלי הנוכחית ונותן לקלוד הוראות חדשות לגמרי לביצוע.

#טיפול בשאלות הבהרה

כש-Claude צריך יותר כיוון במשימה עם כמה גישות תקפות, הוא קורא לכלי AskUserQuestion. זה מפעיל את ה-callback canUseTool שלך כש-toolName מוגדר ל-AskUserQuestion. הקלט מכיל את השאלות של Claude כאפשרויות בחירה, שאתה מציג למשתמש ומחזיר את הבחירות שלו.

[!TIP]

שאלות הבהרה נפוצות במיוחד במצב plan, שבו Claude בוחן את בסיס הקוד ושואל שאלות לפני שהוא מציע תוכנית. זה הופך את מצב plan למתאים במיוחד לתהליכי עבודה אינטראקטיביים שבהם אתה רוצה ש-Claude יאסוף דרישות לפני ביצוע שינויים.

השלבים הבאים מראים איך לטפל בשאלות הבהרה:

1. העברת callback מסוג canUseTool

העבר callback מסוג canUseTool באפשרויות השאילתה שלך. כברירת מחדל, AskUserQuestion זמין. אם אתה מציין מערך tools כדי להגביל את היכולות של Claude (לדוגמה, סוכן לקריאה בלבד עם Read, Glob ו-Grep בלבד), כלול את AskUserQuestion במערך הזה. אחרת, Claude לא יוכל לשאול שאלות הבהרה:

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)
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) => {
      // Placeholder that approves every call. The Detect AskUserQuestion step replaces it.
      return { behavior: "allow", updatedInput: input };
    }
  }
})) {
  console.log(message);
}

2. זיהוי AskUserQuestion

ב-callback שלך, בדוק אם toolName שווה ל-AskUserQuestion כדי לטפל בו אחרת משאר הכלים:

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)
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);
};

3. ניתוח קלט השאלה

הקלט מכיל את השאלות של Claude במערך questions. לכל שאלה יש question (הטקסט להצגה), 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
    }
  ]
}

ראה פורמט השאלה לתיאור מלא של השדות.

4. איסוף תשובות מהמשתמש

הצג את השאלות למשתמש ואסוף את הבחירות שלו. איך עושים את זה תלוי באפליקציה שלך: בקשת קלט בטרמינל, טופס אינטרנט, תיבת דו-שיח בנייד, וכן הלאה.

5. החזרת תשובות ל-Claude

בנה את אובייקט answers כרשומה שבה כל מפתח הוא טקסט ה-question וכל ערך הוא ה-label של האפשרות שנבחרה:

מאובייקט השאלההשתמש כ
שדה question (לדוגמה, "How should I format the output?")מפתח
שדה label של האפשרות שנבחרה (לדוגמה, "Summary")ערך

בשאלות עם בחירה מרובה, העבר מערך של תוויות או חבר אותן עם ", ". אם אתה תומך בקלט טקסט חופשי, השתמש בטקסט המותאם אישית של המשתמש כערך.

return PermissionResultAllow(
    updated_input={
        "questions": input_data.get("questions", []),
        "answers": {
            "How should I format the output?": "Summary",
            "Which sections should I include?": ["Introduction", "Conclusion"],
        },
    }
)
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 לכל אפשרות כדי שהאפליקציה שלך תוכל להציג הדמיה חזותית לצד ה-label. בלי ההגדרה הזו, Claude לא מייצר תצוגות מקדימות והשדה חסר.

previewFormatpreview מכיל
לא מוגדר (ברירת מחדל)השדה חסר. Claude לא מייצר תצוגות מקדימות.
"markdown"אמנות ASCII ובלוקי קוד מגודרים
"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 שואל שאלות הבהרה כשהוא צריך קלט מהמשתמש כדי להמשיך. לדוגמה, כשמבקשים ממנו עזרה בהחלטה על tech stack לאפליקציית מובייל, Claude עשוי לשאול על cross-platform מול native, העדפות backend, או פלטפורמות יעד. השאלות האלה עוזרות ל-Claude לקבל החלטות שתואמות את ההעדפות של המשתמש במקום לנחש.

הדוגמה הזו מטפלת בשאלות האלה באפליקציית טרמינל. זה מה שקורה בכל שלב:

  1. ניתוב הבקשה: ה-callback canUseTool בודק אם שם הכלי הוא "AskUserQuestion" ומנתב ל-handler ייעודי
  2. הצגת שאלות: ה-handler עובר על מערך questions ומדפיס כל שאלה עם אפשרויות ממוספרות
  3. איסוף קלט: המשתמש יכול להזין מספר כדי לבחור אפשרות, או להקליד טקסט חופשי ישירות (לדוגמה, "jquery", "i don't know")
  4. מיפוי תשובות: הקוד בודק אם הקלט מספרי (משתמש ב-label של האפשרות) או טקסט חופשי (משתמש בטקסט ישירות)
  5. החזרה ל-Claude: התשובה כוללת גם את מערך questions המקורי וגם את מיפוי answers

שמור את גרסת TypeScript בשם ask.ts והרץ אותה עם npx tsx ask.ts, או שמור את גרסת Python בשם ask.py והרץ אותה עם python ask.py.

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())
import { query } from "@anthropic-ai/claude-agent-sdk";
import type { PermissionResult } 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): Promise<PermissionResult> {
  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();

#מגבלות

  • סוכני משנה: AskUserQuestion אינו זמין כרגע בסוכני משנה שנוצרים דרך כלי Agent
  • מגבלות שאלות: כל קריאה ל-AskUserQuestion תומכת ב-1-4 שאלות עם 2-4 אפשרויות כל אחת

#דרכים נוספות לקבל קלט מהמשתמש

ה-callback canUseTool והכלי AskUserQuestion מכסים את רוב תרחישי האישור וההבהרה, אבל ה-SDK מציע דרכים נוספות לקבל קלט ממשתמשים:

#קלט בזרימה

השתמש בקלט בזרימה כשאתה צריך:

  • לקטוע את הסוכן באמצע משימה: שלח אות ביטול או שנה כיוון בזמן ש-Claude עובד
  • לספק הקשר נוסף: הוסף מידע ש-Claude צריך בלי לחכות שהוא יבקש אותו
  • לבנות ממשקי צ'אט: אפשר למשתמשים לשלוח הודעות המשך במהלך פעולות ארוכות

קלט בזרימה מתאים לממשקי משתמש שיחתיים שבהם משתמשים מקיימים אינטראקציה עם הסוכן לאורך כל הביצוע, לא רק בנקודות ביקורת של אישור.

#כלים מותאמים אישית

השתמש בכלים מותאמים אישית כשאתה צריך:

  • לאסוף קלט מובנה: לבנות טפסים, אשפים או תהליכי עבודה מרובי שלבים שחורגים מפורמט הבחירה המרובה של AskUserQuestion
  • לשלב מערכות אישור חיצוניות: להתחבר לפלטפורמות קיימות של כרטיסים, תהליכי עבודה או אישורים
  • לממש אינטראקציות ייעודיות לתחום: ליצור כלים מותאמים לצרכי האפליקציה שלך, כמו ממשקי סקירת קוד או רשימות בדיקה לפריסה

כלים מותאמים אישית נותנים לך שליטה מלאה על האינטראקציה, אבל דורשים יותר עבודת מימוש מאשר שימוש ב-callback המובנה canUseTool.

#משאבים קשורים