Skip to main content
会話とセッション

承認とユーザー入力

Agent の実行中にユーザーの操作が必要になるのは、主に次の 2 つの場合です。
  1. Agent がツールを実行しようとしており、ユーザーの許可が必要な場合。
  2. Agent に必要な情報がなく、ユーザーの回答が必要な場合。
ホストアプリケーションが canUseTool(Python では can_use_tool)でユーザー操作を処理する場合、どちらのリクエストも同じコールバックに届きます。アプリケーションは確認画面または質問を表示し、ユーザーの選択を SDK に返します。
ユーザーに表示する内容代表的なツールユーザーが入力するものアプリケーションが返すもの
ツール承認BashWrite、MCP ツールこの操作を許可するかどうか元のツール入力、または確認後に修正した入力
確認質問AskUserQuestion質問への実際の回答questionsanswers
このコールバックは、単一メッセージ入力とストリーミング入力のどちらの prompt にも設定できます。1 回限りの query() でもツール承認や確認質問を表示できます。

canUseTool が呼ばれるタイミング

Agent がツールの使用を要求すると、SDK は最初に権限設定を評価します。その結果は次の 3 つです。
  • すでに許可されている:canUseTool を呼ばずにツールを実行します。
  • すでに拒否されている:canUseTool を呼ばずにツールを拒否します。
  • ユーザーの確認が必要:SDK が canUseTool を呼び出し、アプリケーションから許可または拒否が返るのを待ちます。
AskUserQuestion は通常のツールとは異なり、実際のユーザー回答が必要です。canUseTool / can_use_tool が設定されている場合、ユーザー操作が明示的に禁止されていなければ、SDK は回答を受け取るためにコールバックを呼び出します。 canUseTool はすべてのツール呼び出しを受け取るわけではないため、完全なツール実行ログには使用できません。すべてのツール呼び出しを監視するには hooks を使用してください。権限設定の評価順序は権限制御を参照してください。 アプリケーションで動的な承認または AskUserQuestion を処理するには、canUseTool / can_use_tool か、外部 permission prompt tool を設定する必要があります。ランタイムが権限リクエストを送信したときに対応するコールバックが SDK にない場合、SDK は fail closed としてエラーを報告し、ツールを自動実行しません。

コールバックを設定する

次のコードはコールバックの設定方法だけを示しています。showApprovalDialog / show_approval_dialog はホストアプリケーション側で実装する確認 UI であり、SDK の関数ではありません。実行可能なターミナル版は後述の完全な例を参照してください。
import { accessTokenFromEnv, query } from '@qoder-ai/qoder-agent-sdk';

query({
  prompt: 'リリース設定を読み、changelog を生成してください。ファイルを書き込む前に承認を求めてください。',
  options: {
    auth: accessTokenFromEnv(),
    permissionMode: 'default',
    allowedTools: ['Read'],
    async canUseTool(toolName, input, context) {
      const approved = await showApprovalDialog({
        toolName,
        input,
        title: context.title,
        description: context.description,
        signal: context.signal,
      });

      if (!approved) {
        return {
          behavior: 'deny',
          message: 'ユーザーがこの操作を拒否しました。',
          toolUseID: context.toolUseID,
        };
      }

      return {
        behavior: 'allow',
        updatedInput: input,
        toolUseID: context.toolUseID,
      };
    },
  },
});
TypeScript の context.signalAbortSignal、Python の context.signalasyncio.Event | None です。タスクがキャンセルされた場合、確認画面を閉じてコールバックを終了してください。TypeScript の例では、処理中のツール呼び出しを識別するために toolUseID を返しています。 アプリケーションが確認 UI を提供する場合は canUseTool / can_use_tool を使用します。外部 permission prompt tool がすでにある場合は、代わりに permissionPromptToolName / permission_prompt_tool_name を使用します。2 つを同時に設定することはできません。

承認結果を返す

一度だけ許可する

ツールの実行を許可する場合は、ツール入力を返します。
return {
  behavior: 'allow',
  updatedInput: input,
  toolUseID: context.toolUseID,
};
updatedInput / updated_input はツールが最終的に受け取る入力です。そのまま返すことも、許可する前に危険なオプションを削除する、ファイルパスを制限する、その他のフィールドを変更することもできます。変更後の入力は、そのツールの入力形式に従って検証してください。

拒否する

拒否する場合は、短い理由を指定してください。Agent はその理由に基づいて別の方法を選ぶか、操作を実行しなかった理由をユーザーに説明できます。
return {
  behavior: 'deny',
  message: '本番環境ではこのコマンドを実行できません。',
  toolUseID: context.toolUseID,
};
拒否した後にタスク全体をすぐ停止する場合は、interrupt: true(Python では interrupt=True)を設定します。

このセッションでは常に許可する

context.suggestions には、SDK が保存を推奨する権限ルールが含まれる場合があります。ユーザーが「このセッションでは常に許可」を選んだ場合、その候補を allow の結果に含めます。
return {
  behavior: 'allow',
  updatedInput: input,
  updatedPermissions: context.suggestions,
  toolUseID: context.toolUseID,
};
このルールにより、同種の操作について現在のセッション内で繰り返し確認されるのを防げます。完全な PermissionUpdate 型とカスタムルールの作成方法は、セッション中の権限更新を参照してください。

権限モードがコールバックに与える影響

permissionMode / permission_mode は、そのセッションにおけるツールリクエストのデフォルト動作を決めます。ユーザーに確認画面を表示するか、canUseTool を呼び出すかにも影響します。次の表は、canUseTool / can_use_tool が設定され、AskUserQuestiontools から見えることを前提としています。
権限モード通常のツールAskUserQuestion
default確認が必要なツールはコールバックを呼び出すユーザーの回答を得るためにコールバックを呼び出す
acceptEdits一般的なファイル編集は直接許可し、その他のツールでは確認が必要な場合があるユーザーの回答を得るためにコールバックを呼び出す
plan実際の変更は行わず、必要に応じて確認を要求できる計画や要件を確認するために質問できる
autoSDK が一部のツールを自動的に許可または拒否するため、毎回コールバックが呼ばれるとは限らないユーザーの回答を得るためにコールバックを呼び出す
dontAsk事前に許可されていないツールは確認を表示せずに拒否する質問も拒否されるため、ユーザーの回答を待てない
bypassPermissions / yolo通常のツールは確認なしで実行する引き続きコールバックを呼び出す。SDK がユーザーの代わりに回答することはない
次の制約に注意してください。
  • disallowedTools / disallowed_tools または deny ルールで AskUserQuestion を禁止すると、Agent はユーザーに質問できません。
  • tools を明示的に設定する場合は AskUserQuestion を含めてください。含めないと Agent からこのツールが見えません。
  • bypassPermissionsyolo は信頼できる環境でのみ使用してください。通常のツール承認は省略しますが、ユーザーの代わりに質問へ回答することはありません。
  • 未承認のすべての操作を独自の確認 UI に表示する場合は、通常 default を使用し、そのツールを allowedTools / allowed_tools に追加しないでください。
各モードの完全な動作とリスクは、デフォルトポリシー:permissionModeを参照してください。

AskUserQuestion を処理する

AskUserQuestion は、現在のタスクを続ける前に少数の構造化された質問を行うために使用します。たとえば、対象環境、実装方法、出力形式を選択してもらう場合です。これは新しいチャットメッセージではありません。ユーザーが回答すると、Agent は現在のタスクを再開します。 canUseTooltoolName === 'AskUserQuestion' を受け取ったとき、コールバックの input は次のランタイム構造です。
type AskUserQuestionRuntimeInput = {
  questions: Array<{
    question: string;
    header: string;
    options: Array<{
      label: string;
      description: string;
      preview?: string;
    }>;
    multiSelect?: boolean;
  }>;
};
  • 1 回のリクエストには 1~4 個の質問が含まれます。
  • 各質問には短い header と 2~4 個の選択肢があります。
  • multiSelectfalse または省略されている場合は 1 項目、true の場合は複数項目を選択できます。
  • UI では、選択肢にない回答もユーザーが入力できるようにしてください。
  • 選択肢には preview が含まれる場合があります。TypeScript では、toolConfig.askUserQuestion.previewFormat でプレビューを markdown または html として解釈するよう指定できます。Python SDK には現在、対応する tool_config オプションがありません。
canUseTool コールバックでは、ここに示すランタイムの questions / answers 構造を使用してください。単数形の question / answer フィールドとして処理しないでください。

質問への回答を返す

回答を送信するときも behavior: 'allow' を返します。ここでの allow は「この回答を受け入れてタスクを続ける」という意味です。updatedInput には質問と回答の両方を含めます。
{
  behavior: 'allow',
  updatedInput: {
    questions: input.questions,
    answers: {
      'どの環境にデプロイしますか?': 'Staging',
      'どのチェックを有効にしますか?': '型チェック, 単体テスト',
      'その他の要件はありますか?': 'Node.js 18 との互換性を維持',
    },
  },
}
answers は次の規則でエンコードします。
  • 各 key には、対応する質問の完全な question テキストを使用します。
  • 単一選択では選択肢の label、自由入力ではユーザーが入力したテキストを使用します。
  • 複数選択では、複数の label を , で連結した 1 つの文字列にします。
  • ユーザーが質問をキャンセルした場合は、理由を付けて deny を返します。架空の回答や空白の回答で処理を続けないでください。

完全な例

次の例は、質問と通常のツール承認の両方を処理します。実際のアプリケーションでは、ターミナル入力をダイアログ、Web ページ、承認サービスに置き換えられます。
import { createInterface } from 'node:readline/promises';
import {
  accessTokenFromEnv,
  query,
  type CanUseTool,
} from '@qoder-ai/qoder-agent-sdk';

type Question = {
  question: string;
  header: string;
  options: Array<{ label: string; description: string }>;
  multiSelect?: boolean;
};

const readline = createInterface({
  input: process.stdin,
  output: process.stdout,
});

function parseAnswer(raw: string, question: Question): string {
  const indexes = raw
    .split(',')
    .map((part) => Number.parseInt(part.trim(), 10) - 1)
    .filter((index) => index >= 0 && index < question.options.length);

  if (indexes.length > 0) {
    const selected = question.multiSelect ? indexes : indexes.slice(0, 1);
    return selected.map((index) => question.options[index].label).join(', ');
  }

  return raw.trim();
}

async function readLine(
  prompt: string,
  signal: AbortSignal,
): Promise<string | null> {
  try {
    return await readline.question(prompt, { signal });
  } catch (error) {
    if (
      signal.aborted ||
      (error instanceof Error && error.name === 'AbortError')
    ) {
      return null;
    }
    throw error;
  }
}

const canUseTool: CanUseTool = async (toolName, input, context) => {
  if (context.signal.aborted) {
    return {
      behavior: 'deny',
      message: 'リクエストがキャンセルされました。',
      toolUseID: context.toolUseID,
    };
  }

  if (toolName === 'AskUserQuestion') {
    const questions = (input.questions ?? []) as Question[];
    const answers: Record<string, string> = {};

    for (const question of questions) {
      console.log(`\n${question.header}${question.question}`);
      question.options.forEach((option, index) => {
        console.log(`${index + 1}. ${option.label}${option.description}`);
      });

      const hint = question.multiSelect
        ? '選択肢の番号をカンマ区切りで入力するか、自由回答を入力してください(/cancel でキャンセル):'
        : '選択肢の番号または自由回答を入力してください(/cancel でキャンセル):';

      let answer = '';
      while (!answer) {
        const raw = await readLine(hint, context.signal);
        if (raw === null || raw.trim() === '/cancel') {
          return {
            behavior: 'deny',
            message: 'ユーザーが質問をキャンセルしました。',
            toolUseID: context.toolUseID,
          };
        }

        answer = parseAnswer(raw, question);
        if (!answer) {
          console.log('回答は空にできません。もう一度入力してください。');
        }
      }

      answers[question.question] = answer;
    }

    return {
      behavior: 'allow',
      updatedInput: { questions, answers },
      toolUseID: context.toolUseID,
    };
  }

  const answer = await readLine(
    `\n${toolName} を入力 ${JSON.stringify(input)} で実行することを許可しますか?[y/N] `,
    context.signal,
  );

  if (answer === null) {
    return {
      behavior: 'deny',
      message: 'リクエストがキャンセルされました。',
      toolUseID: context.toolUseID,
    };
  }

  if (answer.trim().toLowerCase() !== 'y') {
    return {
      behavior: 'deny',
      message: `ユーザーが ${toolName} の実行を拒否しました。`,
      toolUseID: context.toolUseID,
    };
  }

  return {
    behavior: 'allow',
    updatedInput: input,
    toolUseID: context.toolUseID,
  };
};

try {
  for await (const message of query({
    prompt: 'プロジェクトを確認してリリースノートを作成してください。リリース先が不明な場合は先に質問してください。',
    options: {
      auth: accessTokenFromEnv(),
      tools: ['AskUserQuestion', 'Read', 'Write', 'Bash'],
      allowedTools: ['Read'],
      permissionMode: 'default',
      canUseTool,
    },
  })) {
    if (message.type === 'result') {
      console.log(message.subtype);
    }
  }
} finally {
  readline.close();
}
Python のターミナル例はブロッキング input() を使用するため、タスクがキャンセルされても現在の入力が戻るまで context.signal を確認できません。Web またはデスクトップアプリケーションではこの制約を引き継がず、context.signal を監視し、シグナル発生時にダイアログをすぐ閉じてコールバックを終了してください。

機能の使い分け

要件推奨機能理由
ツールを実行してよいか確認するcanUseTool / can_use_tool許可、拒否、またはツール入力の修正ができる
Agent が続行する前に 1~4 個の短い回答を得るAskUserQuestionAgent が質問と選択肢を提示し、ユーザーの回答後に現在のタスクを続ける
ユーザーが追加質問、長いテキストの追加、タスクの方向変更を行うストリーミング入力新しいユーザーメッセージとして扱う必要がある
固定フィールド、厳密な検証、ファイルアップロード、複雑なフォームが必要カスタムツールアプリケーションがデータ形式と UI を定義できる
MCP server がフォームまたは認証情報を要求するMCP Elicitationこのリクエストでは onElicitation / on_elicitation を使用する
すべてのツール呼び出しを記録する、または統一的にインターセプトするHooks権限制御canUseTool は自動的に許可または拒否された呼び出しを受け取らない
AskUserQuestion をマルチターン会話の代わりに使用しないでください。ユーザーが自発的に送る新しいメッセージにはストリーミング入力を使用します。また、通常のテキスト応答をツール承認の代わりに使用しないでください。SDK には明示的な allow または deny の結果が必要です。