Claude Media
Agent SDKのstartup()とWarmQueryで初回queryの起動待ちを消す

Agent SDKのstartup()とWarmQueryで初回queryの起動待ちを消す

TypeScript Agent SDKのstartup()は、サブプロセスの起動と初期化を先に終わらせます。WarmQueryの使い方、initializeTimeoutMs、prewarm()との使い分けを解説します。

TypeScript版のAgent SDKでは、query() を呼ぶたびにClaude Codeのサブプロセスが立ち上がり、初期化のやり取りを終えてから最初のメッセージが流れます。startup() は、この起動と初期化をプロンプトが届く前に済ませておく関数です。返ってくる WarmQuery に、あとからプロンプトを渡します。

Slack botやWebのチャットのように、リクエストが来た瞬間に応答を始めたい用途で効きます。作業ディレクトリが決まらない段階で先に起動したい場合は、別の関数 prewarm() を使います。

startup()は何を先に終わらせるのか

startup() は、CLIのサブプロセスを起動し、initializeハンドシェイクを完了させたところで Promise<WarmQuery> を返します。プロンプトはまだ要りません。

戻り値の WarmQuery にプロンプトを渡すと、すでに準備の済んだプロセスへ書き込まれます。最初の query() がサブプロセスの起動と初期化を待たずに進む、というのが仕組みの全体です。

シグネチャは次のとおりです。

function startup(params?: {
  options?: Options;
  initializeTimeoutMs?: number;
}): Promise<WarmQuery>;

options は query() に渡すものと同じ Options です。maxTurns やモデル、権限モードなど、セッションの性格を決める値はこの時点で渡します。

initializeTimeoutMsで待ち時間の上限を決める

initializeTimeoutMs は、サブプロセスの初期化を待つ最大時間(ミリ秒)です。既定値は60000で、時間内に初期化が終わらないとPromiseがタイムアウトエラーで拒否されます。

アプリ起動時に呼ぶ場合、このエラーを握りつぶすと「warmのつもりが実はcold」という状態になります。拒否されたら通常の query() に切り替える分岐を入れておくと、起動の失敗がそのままサービス停止にはなりません。起動失敗の理由そのものを取りたいときは、CLAUDE_CODE_STARTUP_FAILURE_RESULTSで起動失敗の理由を取得するが参考になります。

WarmQueryの使い方と制約

アプリ起動時に startup() を呼び、プロンプトが決まった時点で .query() を呼びます。

import { startup } from "@anthropic-ai/claude-agent-sdk";
 
// 起動時にサブプロセスの準備を済ませる
const warm = await startup({ options: { maxTurns: 3 } });
 
// プロンプトが決まったら、待ちなしで始まる
for await (const message of warm.query("What files are here?")) {
  console.log(message);
}

これはTypeScriptリファレンスに載っている例に沿った形です。WarmQuery が持つメソッドは2つだけです。

メソッド役割
query(prompt)役割準備済みのサブプロセスへプロンプトを送り、Query を返す。1つの WarmQuery につき1回だけ呼べる
close()役割プロンプトを送らずにサブプロセスを閉じる。不要になったwarmを捨てるときに使う

1回しか使えない点が設計を決める

query(prompt) は1つの WarmQuery につき1回です。warmの個数がそのまま「待ちなしで受けられるリクエスト数」になります。1つ使い切ったら、次のために新しい startup() を呼び直す必要があります。

WarmQuery は AsyncDisposable を実装しています。await using で宣言すれば、スコープを抜けるときに自動で後始末されます。プロンプトを送らないまま破棄する経路があるなら、close() を呼ぶか await using に任せておくと、待機中のプロセスが残りません。

リクエストごとに補充する例

次は、公式の例を土台にした自作の補充パターンです。公式が示す形ではなく、1回しか使えない制約への対処として組んだ一例です。

import { startup, type WarmQuery } from "@anthropic-ai/claude-agent-sdk";
 
const options = { maxTurns: 3 };
let next: Promise<WarmQuery> = startup({ options });
 
export async function ask(prompt: string) {
  const warm = await next;
  next = startup({ options }); // 使った分をすぐ補充する
  return warm.query(prompt);
}

next を使い回すと、同時に2件来たときに同じ WarmQuery を取り合います。同時リクエストがあるなら、補充の仕組みを数件ぶんのキューにするか、溢れた分は通常の query() に回します。この例は初期化の失敗も扱っていません。実運用では、前節の拒否時の分岐と合わせて組んでください。

初期化が拒否されたときに通常のquery()へ戻す

先の補充の例に、フォールバックを足すと次の形になります。これも自作の一例です。startup() が拒否されたら、warmを諦めて同じ options で query() を呼びます。

import { query, startup } from "@anthropic-ai/claude-agent-sdk";
 
const options = { maxTurns: 3 };
 
export async function ask(prompt: string) {
  try {
    const warm = await startup({ options, initializeTimeoutMs: 15000 });
    return warm.query(prompt);
  } catch (error) {
    console.warn("startup failed, falling back to query():", error);
    return query({ prompt, options });
  }
}

この書き方は、リクエストごとに startup() を呼ぶので、先に温めておく効果は出ません。形を示すためのもので、実際には「起動時に温めたwarmを使い、失敗したときだけ query() に落とす」構成にします。タイムアウトを短くすれば、初期化が詰まったときにユーザーを長く待たせずに済みます。短くしすぎると、負荷の高い環境で正常な起動まで拒否されます。値は自分の環境の起動時間を測って決めてください。

作業ディレクトリが未定ならprewarm()

startup() に渡す options は起動の時点で決まる値です。ユーザーがフォルダを選ぶまで作業ディレクトリが分からないアプリでは、起動時に startup() を呼べません。この場面のために用意されているのが prewarm() です。

prewarm() は「アルファ」と表記された機能で、TypeScript Agent SDK v0.3.282以降が必要です。セッションが決まる前にClaude Codeプロセスを予備として起動しておき、あとから claim() でセッションに結び付けます。

待機中の予備プロセスが持つもの

prewarm() もinitializeハンドシェイクまでは startup() と同じです。待機する場所は、options.cwd を指定していればそのディレクトリ、なければClaude Codeの設定ディレクトリの下に作られる専用の一時ディレクトリです。

次のものは、claimされるまで待たされます。

  • セッションの作業ディレクトリ
  • SessionStart フック
  • stdioのMCPサーバー
  • CLAUDE.mdとgitの文脈

予備1つが待機中に抱えるメモリは、およそ230〜260MBです。常時いくつ持つかは、この数字から逆算できます。startup() については、待機中のメモリ量の記載がリファレンスにありません。

claim()でセッションに結び付ける

import { prewarm } from "@anthropic-ai/claude-agent-sdk";
 
// アプリ起動時。フォルダはまだ決まっていない
const spare = await prewarm({ options: { maxTurns: 3 } });
 
// ユーザーがフォルダを選んだとき
const claimedQuery = spare.claim({
  prompt: "What files are here?",
  options: { cwd: "/path/to/project" },
});
 
spare.claimed.catch((error: Error) => {
  // 先頭が "option_not_applied" でなければ、プロンプトは実行されていない
  console.error("Claim failed:", error.message);
});

claim() は query() と同じく Query を同期的に返し、1回しか呼べません。options.cwd は必須です。claimで指定できるのは、additionalDirectories、model、permissionMode、maxThinkingTokens、settings のオーバーレイ、appendSystemPrompt、title、agents、env のセッション単位トークンです。

claimが断られる場合

Claude Codeは、存在しないフォルダや、プロジェクト設定が env・agent・model を指定しているフォルダのclaimを拒否することがあります。拒否されると、すでに送ったプロンプトには not_claimed で始まるエラー結果が返り、その後 Query が例外を投げます。ループをtryで囲めば、例外の先に進めます。

claimed のPromiseが拒否された場合の扱いは、メッセージの先頭で分かれます。

  • option_not_applied で始まる: セッションは、頼んだ model や maxThinkingTokens が効かない状態で動いている
  • それ以外: プロンプトは実行されていないので、query() で改めて始める

固定される設定と、1つのスペアの考え方

claimで変えられないものがあります。mcpServers、hooks、canUseTool、settingSources、systemPrompt、plugins などは予備の寿命のあいだ固定です。これらの組み合わせが違うなら、予備もそのぶん別に持ち、設定が変わったら prewarm() をやり直します。

resume、continue、forkSession を options に入れると prewarm() は例外を投げます。予備にはまだセッションがないためです。既存セッションの再開を速くしたい用途には使えません。

spawnClaudeCodeProcess でClaude Codeを別マシンやコンテナで動かしている場合は、options.cwd に向こう側に実在するディレクトリを指定します。予備がそこで待つためです。SpareProcess の exited は、プロセスが終了したときに確定します。claim前に落ちた予備を差し替える合図として使えます。

startup()とprewarm()の使い分け

状況選ぶ関数
起動時に作業ディレクトリやオプションが決まっている選ぶ関数startup()
フォルダ選択のあとでセッションが始まる選ぶ関数prewarm()
安定した機能だけで組みたい選ぶ関数startup()(prewarm() はアルファ)
既存セッションの再開が目的選ぶ関数どちらも向かない

迷ったら startup() から入るのが素直です。後から作業ディレクトリが可変になった時点で prewarm() を検討する流れになります。

/coreエントリにはprewarm() が無い

アプリがAgent SDKを自前の依存と一緒にバンドルする場合は、パッケージルートでなく @anthropic-ai/claude-agent-sdk/core から読み込めます。/core はv0.3.282以降で使えます。

/core は query()、startup()、tool()、createSdkMcpServer()、resolveSettings() を、ルートと同じ名前で出しています。一方で、読み込むコードを小さく保つために、prewarm()、InMemorySessionStore クラス、セッションの一覧・読み取り・フォーク・取り込み・要約を扱うヘルパーなどを外しています。

つまり、バンドルを小さくしたいアプリで prewarm() を使いたいときは、/core ではなくルートエントリから読み込む必要があります。注意点がもう1つあります。1つのプロセスの中でルートと /core の両方を読み込まないでください。別のバンドルなので、クラスや状態が2組できます。

// /core から startup() は使える
import { startup } from "@anthropic-ai/claude-agent-sdk/core";
 
// prewarm() はルートからだけ
// import { prewarm } from "@anthropic-ai/claude-agent-sdk";

常駐サーバーでの置き場所

ホスティングの文書は、長時間動くセッション型の構成で startup() を使う場面を挙げています。コンテナがHTTPかWebSocketのエンドポイントを出し、アクティブなセッションごとに長く生きる query() とその裏のサブプロセスを対応させる構成です。

TypeScriptでは、streamInput() で動いているセッションにターンを追加し、startup() で着信の前にサブプロセスを温めます。セッションの作業ディレクトリが最初のリクエストまで分からないときは、prewarm() で温めます。サブプロセスとセッションの設計全体は、Agent SDK本番ホスティングで扱っています。

Slack botの返答が遅いときの切り分けでも、query() は呼び出しのたびにサブプロセスを起動するため、startup() で先に起動する手があります。手順はSlack常駐botのミニチュートリアルにあります。

Pythonでは同じ手が使えない

Python版のリファレンスには startup() に相当する関数がありません。Pythonでセッションを開いたまま保つ手段として挙がっているのは ClaudeSDKClient です。

TypeScriptのコードを移植するときは、startup() の呼び出しをそのまま置き換えることはできません。ターンをまたいで同じクライアントを保持して、2回目以降の起動を省く設計に組み替える形になります。入門の比較表でも、起動の事前ウォームはTypeScriptだけの機能として整理されています。全体像はClaude Agent SDK入門で見られます。

導入前に押さえたい点

  • warmは1回きりなので、同時リクエストの数に合わせた補充を設計に含める
  • initializeTimeoutMs の既定は60000。拒否されたときの代替経路を用意する
  • 使われなかったwarmは close() か await using で必ず片付ける
  • 作業ディレクトリが未定なら prewarm()。ただしアルファで、メモリは1つ230〜260MB
  • /core から読み込むなら prewarm() は使えない
  • 起動時間がどれだけ縮むかの数値は、リファレンスに載っていない。自分の環境で計測する

最後の点は、導入判断で効きます。startup() が消すのはサブプロセスの起動と初期化のぶんです。モデルの応答時間やツール実行の時間は、warmにしても縮みません。最初のメッセージが出るまでの時間を startup() の有無で比べて、差が出るかを確かめてから常駐させると、メモリを無駄にしません。

この記事を共有:XはてブLinkedIn