【Claude Code 連載 第94回】Agent SDK を本番環境でホストする(agent-sdk/hosting)

スポンサーリンク
【Claude Code 連載 第94回】Agent SDK を本番環境でホストする(agent-sdk/hosting) 用語解説
【Claude Code 連載 第94回】Agent SDK を本番環境でホストする(agent-sdk/hosting)
この記事は約27分で読めます。
よっしー
よっしー

こんにちは。よっしーです(^^)

スポンサーリンク

背景

この連載では、Claude Codeの公式ドキュメントを1ページずつ読み解いていきます。公式ドキュメントは情報が網羅されている分、「結局どの機能を、どんな場面で使えばいいのか」は自分で考える必要があり、読むのに意外と時間がかかります。そこで、私が実務で使うために読み込んだ内容を「使う場面→実例コード」の順に整理して残していくことにしました。専門家の解説というより、一次情報を読んだ記録です。推測や動作を確認していない部分には、その都度そう書きます。

公式ドキュメント:https://code.claude.com/docs/ja/agent-sdk/hosting

1. 一言でいうと

Agent SDK のエージェントは、query() を呼ぶたびに claude CLI を別プロセス(サブプロセス)として起動して動きます。このサブプロセスはシェル・作業ディレクトリ・ディスク上のセッション記録を持ち、長く生きます。そのため、Agent SDK のホスティングは「リクエストを受けて返すだけのステートレスな API」を載せるのとは考え方が違います。このページは、その前提から、セッションの持たせ方、コンテナの用意、記録の永続化、可観測性、認証、台数の見積もり、テナントの分離までを、自前のインフラで運用する立場でまとめたものです。

第76回から続けてきた Agent SDK の各機能を「本番にどう載せるか」でまとめ直す回です。第93回のセッションストアも、ここで配置の中に位置づけられます。


2. どういう場面で役立つか

シーン1:ユーザーごとのタスクをコンテナで使い捨てにしたい

請求書の読み取り、文書の翻訳、バグの調査と修正のような「1回で終わる仕事」を、タスクごとに新しいコンテナで実行し、終わったら捨てる構成です。原文はこれを「エフェメラルセッション」と呼びます。どのコンテナにも前の仕事の痕跡が残らないので、扱いが単純です。

シーン2:チャットボットやメール処理のように、ずっと動かしておきたい

Slack からの問い合わせを受け続けるボットや、届いたメールを仕分けて返信するエージェントは、コンテナを立てっぱなしにして、1つのコンテナで複数のセッションを抱えます。この場合、セッションを特定のコンテナに固定する振り分けや、1台あたり何セッション持てるかの見積もりが必要になります。

シーン3:たまにしか使われないが、続きから再開したい

個人のプロジェクト管理や、数時間おきに止めては再開する調査のように、使われない時間が長いセッションです。待っている間はコンテナを止めて費用を抑え、ユーザーが戻ってきたら起動して続きから再開します。原文はこれを「ハイブリッドセッション」と呼び、第93回の SessionStore が必須になります。

不要・向かないケース

  • エージェントループを自分のインフラで動かす必要がない:原文は、その場合は Managed Agents を検討するよう書いています。Anthropic がエージェントループをホストし、アプリケーションはクライアント SDK か REST API でイベントを送ってストリーミングで結果を受け取ります。ツールの実行は Anthropic が管理するクラウドのサンドボックスか、自分のインフラ上の「セルフホスト型サンドボックス」で行われます。このページ(セルフホスティング)の内容を自分で背負う必要がなくなります。
  • 手元のマシンで試しているだけ:このページの論点(再起動で消える状態、台数、テナント分離)は、本番の運用で初めて問題になるものです。試すだけなら第78回のクイックスタートで足ります。
  • デプロイできる Dockerfile や Kubernetes マニフェストそのものが欲しい:このページは設計の考え方を説明するもので、そのまま使える定義ファイルは公式の「ホスティングクックブック」(GitHub)にあります。原文も「ここでセッションパターンを選び、デプロイ先はクックブックから選ぶ」という分担を示しています。

3. コードと仕組みの解説

原文の fenced コードブロックは10本です(作業ディレクトリを分ける例・エフェメラル用のエントリポイント・ハイブリッドの再開・マルチテナント分離の、それぞれ TypeScript と Python の対訳4組8本と、OpenTelemetry の .env、台数の計算式)。対訳はどれも中身が違うので全数を引用します。コードは原文から機械的に抜き出したもので、入れ子に由来する先頭インデントを外した以外は変えていません。コード中の // ... や Python の ... は、原文が「ここは省略」「値は自分で用意する」の印として書いているものです。

原文の注記として、このページの TypeScript の例はトップレベルの await を使っているので、.mts ファイルとして保存するか、package.json に "type": "module" を設定する必要があります。

3-0. 前提条件とバージョン要件

項目要件
言語ランタイムPython SDK は Python 3.10 以上、TypeScript SDK は Node.js 18 以上
Claude Code 本体両 SDK ともほとんどのインストールでネイティブの Claude Code バイナリを同梱。起動される CLI に別途 Node.js は不要(別のネイティブインストールが要る例外は第78回)
出ていく通信api.anthropic.com への HTTPS(Amazon Bedrock や Google Cloud の Agent Platform で動かす場合は各プロバイダーのリージョナルエンドポイント)。MCP サーバーや外部ツールを使うならその宛先も
env で CLAUDE_CODE_PROJECT_DIR_NAME を使う(マルチテナント分離の任意設定)TypeScript Agent SDK v0.3.234 以降、または Python Agent SDK v0.2.140 以降

同梱のバイナリは SDK パッケージのバージョンに固定されています。SDK を更新することが、CLI を更新する方法です。SDK は semver に従うので、原文はパッチリリースは継続的に取り込み、マイナーリリースは取り込む前に TypeScript/Python それぞれのチェンジログを確認するよう勧めています。

3-1. 大前提:1セッション=1サブプロセス

このページのすべての判断は、SDK がエージェントをどう動かすかに基づいています。コードが query() を呼ぶと、SDK は別の claude CLI プロセスを起動し、標準入出力(stdio)でやりとりします。そのサブプロセスがシェル、作業ディレクトリ、ローカルディスク上の JSONL のセッション記録を持ちます。サブプロセスは外部と HTTPS で api.anthropic.com を呼び、記録はローカルディスクに書きます。

ここから次のことが決まります。

  • 1つのエージェントセッションは1つのサブプロセス。N 個のセッションを同時に動かすことは、N 個のサブプロセスを動かすことで、それぞれが自分のプロセスツリーと記録ファイルを持つ。
  • 既定では、すべてのサブプロセスがアプリケーションの作業ディレクトリを引き継ぐ。セッションごとにファイルシステムを分けたい場合は、query() のオプションで別の cwd を渡す。
import { query } from "@anthropic-ai/claude-agent-sdk";

for await (const message of query({
  prompt: "Summarize the files in this directory",
  options: { cwd: "/work/session-a" },
})) {
  console.log(message);
}
import asyncio

from claude_agent_sdk import ClaudeAgentOptions, query


async def main():
    async for message in query(
        prompt="Summarize the files in this directory",
        options=ClaudeAgentOptions(cwd="/work/session-a"),
    ):
        print(message)


asyncio.run(main())

TypeScript は options のオブジェクトに cwd を、Python は ClaudeAgentOptions の cwd 引数で渡します。中身は同じです。

ローカルディスクに置かれる状態

既定では、次の3種類の状態がコンテナのファイルシステムにあります。どれも、コンテナの再起動・スケールダウン・別ノードへの移動を越えて残りません。

状態既定の場所永続化の手段
セッションの記録(トランスクリプト)~/.claude/projects/、CLAUDE_CONFIG_DIR を設定していればその下の projects/SessionStore アダプター(第93回)
CLAUDE.md メモリファイルユーザー階層は ~/.claude/CLAUDE.md、プロジェクト階層はセッションの作業ディレクトリ自前で用意(マウントしたボリューム、オブジェクトストアとの同期など)
作業ディレクトリの成果物セッションの作業ディレクトリ同上

表の右列は原文の記述を筆者が列にまとめたものです。SessionStore が守るのは記録だけで、エージェントが作ったファイルや CLAUDE.md は別に考える必要がある、というのがこのページで繰り返し出てくる注意です。

3-2. セッションパターンを選ぶ

原文は、コンテナが「それが受け持つセッションに対してどれくらいの期間存在するか」で、4つのパターンを挙げています。

パターンコンテナの寿命向いている仕事(原文の例)
エフェメラルタスクごとに作って、終わったら捨てるバグの調査と修正、請求書・領収書の抽出、文書の翻訳、メディア変換
長時間実行立てっぱなし。1コンテナで複数の SDK プロセスを抱えることが多い届いたメールを仕分けて返信、コンテナのポートでユーザーが編集できるサイトを提供、Slack などからの継続的な問い合わせ
ハイブリッド起動時に SessionStore から復元し、更新を書き戻す。アイドル中は止めるときどき確認する個人のプロジェクト管理、数時間おきに止めて再開する調査、問い合わせのたびにチケット履歴を読むサポート
マルチエージェントコンテナ1コンテナに複数の SDK サブプロセスエージェント同士が共有環境でやりとりするマルチエージェントのシミュレーション

表は筆者のまとめです。以下、パターンごとのコードと注意を見ます。

(a) エフェメラルセッション

コンテナは、環境変数 TASK_PROMPT からタスクを読み、SDK を呼んで終わる、1回きりのエントリポイントを実行します。ユーザーはタスクの実行中はエージェントとやりとりできますが、終わるとコンテナは捨てられます。

import { query } from "@anthropic-ai/claude-agent-sdk";

const prompt = process.env.TASK_PROMPT!;
for await (const message of query({ prompt, options: { maxTurns: 20 } })) {
  console.log(message);
}
import asyncio
import os

from claude_agent_sdk import ClaudeAgentOptions, query


async def main():
    async for message in query(
        prompt=os.environ["TASK_PROMPT"],
        options=ClaudeAgentOptions(max_turns=20),
    ):
        print(message)


asyncio.run(main())

ターン上限は TypeScript が maxTurns: 20、Python が max_turns=20 です。

終了のしかたに注意があります。 スクリプトは届いたメッセージを順に出力し、上限内にタスクが終われば subtype が success の結果メッセージが来ます。一方、20ターンの上限に達した場合は、結果メッセージの subtype が error_max_turns になり、query() はそのメッセージを出した後にエラーを送出します。コンテナをきれいに終了させたい場合は、ループを try ブロックで包む必要があります。上のコードは包んでいないので、上限に達すると例外で終わります(エラーの種類は第77回の「結果を処理する」)。

(b) 長時間実行セッション

コンテナは HTTP か WebSocket のエンドポイントを公開し、アクティブなセッションのそれぞれを、長く続く1つのクエリとその背後のサブプロセスに対応させます。セッションを開いたままにして「温めておく」呼び出しは SDK によって違います。

言語使うもの
TypeScriptstreamInput() でアクティブなセッションにターンを足す。トラフィックが来る前にサブプロセスを事前に起動しておくには startup()。セッションの作業ディレクトリが最初のリクエストまで分からない場合は、代わりに prewarm() で事前起動する
PythonClaudeSDKClient でセッションをターンをまたいで開いたままにする

第91回のストリーミング入力の延長にある使い方です。原文は、コンテナのサイズを、メモリに同時に抱えられる最大のセッション数に合わせるよう書いています(見積もり方は 3-5)。

(c) ハイブリッドセッション

一時的なコンテナが、起動時に SessionStore から状態を復元(原文の言葉では「水和」)し、更新をストアに書き戻します。アイドル中はコンテナを止め、ユーザーが戻ってきたら起動し直します。原文は、プロバイダーのアイドルタイムアウトを、ユーザーが戻ってくると見込む頻度に合わせるよう勧めています。

そして、このパターンではストアは必須で、任意ではありません。SessionStore を設定しないままコンテナを止めると、記録が失われるからです。

パターンの核は、共有のストアをつないだ状態で、ID を指定してセッションを再開することです。

import { query, type SessionStore } from "@anthropic-ai/claude-agent-sdk";

declare const userInput: string;
declare const sessionId: string;          // looked up from your database by user
declare const sessionStore: SessionStore; // an object store, key-value store, database, or your own adapter

for await (const message of query({
  prompt: userInput,
  options: { resume: sessionId, sessionStore },
})) {
  // ...
}
from claude_agent_sdk import query, ClaudeAgentOptions, SessionStore
import asyncio

user_input: str = ...
session_id: str = ...              # looked up from your database by user
session_store: SessionStore = ...  # an object store, key-value store, database, or your own adapter


async def main():
    async for message in query(
        prompt=user_input,
        options=ClaudeAgentOptions(
            resume=session_id,
            session_store=session_store,
        ),
    ):
        ...


asyncio.run(main())

コメントにあるとおり、sessionId はユーザーごとに自分のデータベースから引いてくる値で、sessionStore はオブジェクトストア・キーバリューストア・データベース、または自作のアダプターです。TypeScript は resume と sessionStore、Python は resume と session_store という名前で渡します。第93回で見たとおり、ストアから再開した実行はローカルの写しを終了時に消すので、ストアが唯一の記録になります。

(d) マルチエージェントコンテナ

1つのコンテナで複数の SDK サブプロセスを動かします。原文は2つのことを求めています。

  • エージェントごとに作業ディレクトリを分ける:互いのファイルを上書きしないように。
  • 設定の読み込みを分離する:エージェントごとの CLAUDE.md が他のエージェントに漏れないように。

具体的なオプションは、3-6 のマルチテナント分離と同じです。

3-3. コンテナを用意する

サンドボックスの選び方

原文は、プロセスの分離、リソースの制限、ネットワークの制御、一時的なファイルシステムのために、SDK をサンドボックス化したコンテナの中で動かすよう勧めています。提供者を選ぶときに答えるべき問いは次の5つです。

問い何を見るか
誰がサンドボックスを動かすかサービスとして提供する業者がインフラを運用するのか、自分のサーバーで動かすソフトウェアを提供されるのか
コールドスタートの遅さ「サンドボックスを作る」から「最初のリクエストを受けられる」までの時間。エフェメラルは1秒未満が必要。長時間実行はもっと長くても許される
永続ストレージ耐久性のあるボリュームがあるか、一時ディスクだけか。ハイブリッドはサンドボックスの中か隣のどこかに耐久性のあるストレージが要る
料金体系秒単位・リクエスト単位・時間単位の定額。秒単位は突発的なエフェメラルに、時間単位は長時間実行に向く
ネットワーク独自の出口ルール、外向きプロキシ、規制環境向けのプライベート VPC ピアリングに対応しているか

Docker、gVisor、Firecracker のような自前で動かす選択肢と、その細かな分離設定は、別ページの「セキュアデプロイメント」の「分離テクノロジー」に回されています。

リソースの目安

新しく起動するインスタンスごとに 1 GiB の RAM、5 GiB のディスク、1 CPU が妥当な出発点だと原文は書いています。ただし、メモリ使用量はセッションの長さとツールの活動量に応じて増えます。アイドル時の値ではなく、実際に必要なセッションの長さと同時実行数に合わせて決める必要があります。原文は後の節で、この 1 GiB を「下限であって上限ではない」と念を押しています。

ネットワーク

  • 出ていく通信:3-0 の表のとおり。原文は、本番では出ていく通信をエグレスプロキシ経由にするよう勧めています。プロキシで、宛先ドメインの許可リストを適用し、認証情報を差し込み、リクエストを記録します。
  • 入ってくる通信:コンテナの HTTP か WebSocket のポートを公開し、アプリケーションがそこでクライアントのリクエストを受けて、内部で SDK を呼びます。サブプロセス自体はネットワークで待ち受けません。

3-4. セッションと状態の永続化、可観測性

SessionStore について知っておくべき3点

ローカルディスクは、再起動・スケールダウン・別ノードへの移動で失われます。ユーザーが再開を期待するセッションは、SessionStore アダプターで記録を耐久性のあるストレージに写します。原文はこのページでも、第93回の要点を3つに絞って繰り返しています。

点内容
記録だけSessionStore が写すのは記録だけ。CLAUDE.md や作業ディレクトリの成果物は写さないので、共有ボリュームをマウントするか別途同期する
置き換えではなく写しサブプロセスはまずローカルに書き、SDK がバッチごとの写しをストアに送る。新しいセッションのローカル記録は実行後も残る。ストアから再開した実行は終了時にローカルの写しを消すので、ストアが唯一の耐久性のある写しになる
mirror_errorバッチをストアに届けられないと、SDK はそのバッチを捨て、{ type: "system", subtype: "mirror_error" } メッセージを出して、クエリを続ける。ストアの耐久性が大事なら、これにアラートを設定する

3点目は第93回でも強調した「失敗しても止まらない」性質です。ハイブリッドのようにストアが唯一の記録になる構成では、mirror_error を見張っていないと、記録の欠けに気づけないことになります(筆者の指摘)。

可観測性:OpenTelemetry を環境変数で有効にする

エージェントは長く動くプロセスで、多くの API の往復にわたってツールを呼びます。原文は、テレメトリが無ければ「どのツールが動いたか」「どれだけ時間がかかったか」「セッションがどこで止まったか」が分からない、と書いています。

SDK は OpenTelemetry の設定を環境から引き継ぎます。コンテナかオーケストレーターの層で OTEL の環境変数を設定すれば、すべての query() 呼び出しがスパン・メトリクス・ログイベントをコレクターに送ります。次の例は3つのシグナルすべてを OTLP で送る設定です。

CLAUDE_CODE_ENABLE_TELEMETRY=1
CLAUDE_CODE_ENHANCED_TELEMETRY_BETA=1
OTEL_TRACES_EXPORTER=otlp
OTEL_METRICS_EXPORTER=otlp
OTEL_LOGS_EXPORTER=otlp
OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
OTEL_EXPORTER_OTLP_ENDPOINT=http://collector.example.com:4318
  • CLAUDE_CODE_ENHANCED_TELEMETRY_BETA はトレースにだけ必要です。メトリクスとログだけを送るなら省きます。
  • プロンプトの本文とツールへの入力は、既定では送られません。 含めるためのオプトインのフラグと、送られるシグナルの一覧は、別ページの「可観測性」にあります。

宛先の http://collector.example.com:4318 は例示のアドレスなので、自分のコレクターに置き換えます。

3-5. 認証・台数・費用

認証とシークレット

原文は、ホスティングで重要な認証の論点を3つ挙げています。

論点原文の指示
Anthropic APIサブプロセスは環境から ANTHROPIC_API_KEY を読む。シークレットマネージャーから渡すか、ANTHROPIC_BASE_URL を設定して、コンテナの外でキーを差し込むプロキシ経由でモデルを呼ぶ
入ってくる通信認証はエージェントのコンテナの前に置いたゲートウェイで行う。エージェントは認証済みのリクエストを受け取る側であって、ユーザーのトークンを検証する部品であってはならない
外向きのツールツールの認証情報をエージェントの環境に置かない。外向きの呼び出しをプロキシに通し、リクエストがコンテナを出た後で API キーを差し込む。エージェントが呼び出し、プロキシが認証情報を足す

3つに共通するのは、エージェントのコンテナの中に秘密を置かない方向です。エージェントはツールでシェルやファイルに触れるので、中に置いた秘密はエージェントから読める位置にある、というのが背景だと筆者は解釈しています(原文はこの理由を明示していません)。

1台あたり何セッション持てるか

各セッションは自分のサブプロセスで動くので、1台のホストで同時に動かせる数は、そのホストの RAM が抱えられるサブプロセスの数で決まります。原文は次の式でホストの大きさを決めるよう書いています。

agents per host = (host RAM - overhead) / (per-session RAM ceiling)

「ホストの RAM からオーバーヘッドを引き、1セッションあたりの RAM の上限で割る」という意味です。1セッションあたりの上限は、代表的なセッションを目標の長さまで、想定するツールの負荷のもとで動かし、**ピーク時の RSS(実際に使った物理メモリ)**を記録して測ります。

筆者の仮の数字で使い方を示すと、16 GiB のホストでオーバーヘッドを 2 GiB と置き、測った上限が 1.5 GiB なら、(16 − 2) ÷ 1.5 ≒ 9.3 なので 9 セッションです。ここで出発点の 1 GiB を割る数に使うと 14 セッションとなり、長いセッションが重なったときにメモリが足りなくなります。原文が 1 GiB を「下限であって上限ではない」と書いているのはこのためだと読めます(数字はすべて筆者の例示です)。

横に増やすときの振り分け

振り分け方はパターンによって違います。長時間実行のように1コンテナが多くのセッションを抱える場合は、ロードバランサーの後ろにコンテナのプールを置き、sessionId による一貫性ハッシュ(consistent hashing)で各セッションを1つのコンテナに固定します。固定されたセッションは、削除されるかコンテナが再起動するまで、同じコンテナ、つまり同じ動作中のサブプロセスに当たり続けます。

原文が固定の方法を示しているのは長時間実行だけです。ハイブリッドは毎回ストアから復元するので、どのコンテナに当たってもよい設計になる、と筆者は解釈しています。

費用

Anthropic のトークン費用は、ふつうコンテナの費用より1桁以上大きいと原文は書いています。最小限のコンテナは1時間あたり約 0.05 ドルで動きますが、1つの長いエージェントセッションはトークンで数ドルを使うことがあります。インフラを切り詰めるより、セッションの長さとターン数を管理するほうが費用に効く、ということです。セッションごとの集計は第92回のコスト追跡で行います(その値は推定で請求額ではない点も第92回のとおり)。

3-6. マルチテナント分離

既定の SDK は、ファイルシステムから設定と CLAUDE.md メモリファイルを読みます。複数のテナント(顧客や利用者)を1つのコンテナで扱うと、これらのファイルを通じて、あるテナントの文脈が別のテナントのセッションに漏れるおそれがあります。原文が挙げる対策は5つです。

対策何を防ぐか
TypeScript で settingSources: []、Python で setting_sources=[]ユーザー・プロジェクト・ローカルの設定を読まない
env で CLAUDE_CODE_DISABLE_AUTO_MEMORY=1自動メモリ(~/.claude/projects/<project>/memory/)は settingSources に関係なくシステムプロンプトに読み込まれるので、別に止める
CLAUDE_CONFIG_DIR をテナントごとのディレクトリにするテナントが ~/.claude.json のグローバル設定を共有しない
テナントごとの作業ディレクトリすべての query() 呼び出しで cwd を明示する
プロキシでテナントごとの出口ルールテナントごとに外向きの IP・認証情報・ドメイン許可リストを変え、侵害されたテナントが別テナントの出口ポリシーを使ってデータを持ち出せないようにする

2行目が要注意です。settingSources を空にしても自動メモリは読まれるので、settingSources: [] だけで分離したつもりになると漏れます。settingSources が制御しない入力は他にもあり、原文は別ページ(Claude Code の機能を SDK で使うページの「settingSources が制御しないもの」)を案内しています。

3行目には任意の追加設定があります。各設定ディレクトリが1つの作業ディレクトリだけを受け持ち、テナント間で SessionStore を共有しない場合は、env で CLAUDE_CODE_PROJECT_DIR_NAME を設定して、その下の記録のパスを短くできます(3-0 のバージョン要件あり)。

次の例は、設定・自動メモリ・設定ディレクトリ・作業ディレクトリの4つをまとめて適用します。tenantDir と configDir は、各テナントが、他のテナントから読めないパスを持つように組み立てます。

import { query } from "@anthropic-ai/claude-agent-sdk";

declare const prompt: string;
declare const tenantDir: string;
declare const configDir: string;

for await (const message of query({
  prompt,
  options: {
    cwd: tenantDir,
    settingSources: [],
    env: {
      ...process.env,
      CLAUDE_CONFIG_DIR: configDir,
      CLAUDE_CODE_DISABLE_AUTO_MEMORY: "1",
    },
  },
})) {
  // ...
}
from claude_agent_sdk import query, ClaudeAgentOptions
import asyncio

prompt: str = ...
tenant_dir: str = ...
config_dir: str = ...


async def main():
    async for message in query(
        prompt=prompt,
        options=ClaudeAgentOptions(
            cwd=tenant_dir,
            setting_sources=[],
            env={
                "CLAUDE_CONFIG_DIR": config_dir,
                "CLAUDE_CODE_DISABLE_AUTO_MEMORY": "1",
            },
        ),
    ):
        ...


asyncio.run(main())

ここに言語差があり、取り違えると動きません。

  • TypeScript の env はサブプロセスの環境を置き換えます。 そのため ...process.env で展開して、PATH や ANTHROPIC_API_KEY などの引き継ぐべき変数を残しています。これを書かないと、サブプロセスは PATH も API キーも持たずに起動します。
  • Python の env は引き継いだ環境の上に重ねられます。 だから Python の例には展開が無く、2つの変数だけを書いています。

筆者の指摘として、TypeScript の ...process.env は親プロセスの環境変数をすべてサブプロセスに渡します。3-5 の「ツールの認証情報をエージェントの環境に置かない」と組み合わせて考えると、アプリケーション本体の環境にツールやデータベースの秘密が入っている場合、それもエージェントのサブプロセスに渡ることになります。マルチテナントの構成では、親の環境に何が入っているかを確認しておくべきでしょう。Python も引き継ぐ点は同じです。

3-7. 既知の制限

原文の表をそのまま日本語で示します。

制限対処
セッション全体のタイムアウトがないセッションは自動ではタイムアウトしない。TypeScript の maxTurns、Python の max_turns で、ツール使用の往復回数を制限して止める
長いセッションでメモリが増えるセッションの長さを制限するか、サブプロセスを定期的に作り直す(3-5 の台数の見積もりも参照)
大規模な並列サブエージェントの展開がレート制限に達することがある一度に広く投げず、小さなバッチに分ける
サブエージェントごとの実時間の期限がない各サブエージェントを AgentDefinition の maxTurns で制限する。CLAUDE_ASYNC_AGENT_STALL_TIMEOUT_MS は、サブエージェントが出力を出さなくなったときに発火する停滞の監視タイマーで、総実行時間の期限ではない

1行目と4行目は「時間で止める仕組みは無く、回数で止める」という同じ話です。停滞タイマーは「何も出さなくなった」ことを検知するだけなので、出力を出し続けながら長く走るサブエージェントは止めません(筆者の解釈)。時間で区切りたい場合は、アプリケーション側で別に仕組みを持つことになります。

3-8. デプロイで失敗したとき

手元では動くエージェントが、デプロイしたサービスでは失敗する場合の、3つの典型です。原因の詳細は SDK のトラブルシューティングページに回されています。

症状原因(原文)
サービス起動時に CLI が見つからないPython:コンテナやサービスマネージャーがアプリケーションをシェルとは違う PATH で動かすため、手元で動くインストールがプロセスから見えない。TypeScript:イメージのビルドで SDK の任意依存が飛ばされたか、pathToClaudeCodeExecutable がイメージに無いファイルを指している
イメージに CLI はあるが起動しないコンテナのアーキテクチャや libc と合わないバイナリ、またはイメージのビルド中に実行権限を失ったファイルから起動しようとしている
Claude Code のプロセスが実行中に終了するアプリケーションが受け取るエラーは、SDK の言語と、CLI が先にエラー結果を報告したかどうかで変わる

1行目の TypeScript の原因は、3-0 の「ネイティブバイナリを同梱」と関係します。同梱のバイナリが任意依存として入る作りのため、ビルド設定で任意依存を省くと消える、と読めます(筆者の解釈。原文は「任意依存をスキップした」とだけ書いています)。


4. まとめ

  • query() は claude CLI のサブプロセスを起動する。1セッション=1サブプロセスで、シェル・作業ディレクトリ・記録を持つ長寿命のプロセス。ステートレスな API とは前提が違う。
  • 記録・CLAUDE.md・作業ディレクトリの成果物は、既定ではローカルにあり、再起動で消える。SessionStore が守るのは記録だけ。
  • セッションパターンは4つ:エフェメラル(タスクごとに使い捨て)、長時間実行(立てっぱなし、TS は streamInput()・startup()・prewarm()、Python は ClaudeSDKClient)、ハイブリッド(アイドル中は止めてストアから復元。ストアは必須)、マルチエージェントコンテナ。
  • エフェメラルで maxTurns に達すると error_max_turns の結果の後に例外が出る。きれいに終わらせたいなら try で包む。
  • 出発点は 1 GiB RAM・5 GiB ディスク・1 CPU。ただし 1 GiB は下限。台数は「(ホストの RAM − オーバーヘッド) ÷ 1セッションのピーク RSS」で見積もる。長時間実行は sessionId の一貫性ハッシュでコンテナに固定する。
  • 可観測性は OTEL の環境変数で有効にする。トレースには CLAUDE_CODE_ENHANCED_TELEMETRY_BETA も要る。プロンプトとツール入力は既定で送られない。
  • 認証はゲートウェイで受け、秘密はプロキシで差し込み、エージェントの環境に置かない。
  • 費用はトークンがコンテナより1桁以上大きい。
  • マルチテナントは settingSources: []・CLAUDE_CODE_DISABLE_AUTO_MEMORY=1(settingSources では止まらない)・テナントごとの CLAUDE_CONFIG_DIR と cwd・テナントごとの出口ルール。TypeScript の env は置き換えなので ...process.env が要る。
  • セッションにもサブエージェントにも時間のタイムアウトは無い。回数(maxTurns)で止める。

次回予告

次回は 「セキュアデプロイメント」(agent-sdk/secure-deployment) を取り上げる予定です。

今回、エグレスプロキシで出ていく通信を絞る、秘密はコンテナの外で差し込む、サンドボックスの中で動かす、といった方針が何度も出てきましたが、具体的な設定は別ページに回されていました。次回はその本体として、ネットワークの制御、認証情報の管理、Docker・gVisor・Firecracker などによる分離の強化を、エージェントを安全に本番へ出すための手順として見ていきます。


よっしー
よっしー

何か質問や相談があれば、コメントをお願いします。また、エンジニア案件の相談にも随時対応していますので、お気軽にお問い合わせください。

それでは、また明日お会いしましょう(^^)

コメント

タイトルとURLをコピーしました