ANTIGRAVITY LABEN
記事一覧/連携・プラグイン
連携・プラグイン/2026-08-22中級

stream-json 入力で Antigravity CLI を外部から回すとき、会話をどこで切るか

print モードに加わった --input-format stream-json は、外部のドライバがセッションを開いたまま一件ずつ流せる仕組みです。24 件のジョブで会話が抱える量を計測し、バイト予算だけで切ると仕事の途中で文脈が切れることを確かめ、切る単位の決め方を整理します。

Antigravity CLI29stream-jsonバッチ処理3自動化37Node.js5

プレミアム記事

個人開発で抱えている 6 本のアプリを更新するたび、言語ごとのリリースノートを書き直しています。日本語で書いて、英語に直して、韓国語と繁体字を用意する。1 回の更新で 24 本の短い文章が必要になります。

これをエージェントに任せるとき、これまでは 1 件ごとにコマンドを起動していました。24 回の起動、24 回のサインイン確認、そして 24 回とも「前の言語でこう書いた」という前提が消えます。3 本目のアプリの英語版だけ用語が揺れる、という事故はここから来ます。

CLI 1.1.15(8 月 19 日)の print モードに --input-format stream-json が入りました。改行区切りの JSON を標準入力から読み、1 メッセージにつき 1 ターンを同じ会話の中で実行します。外部のドライバがセッションを開いたまま回せる形です。

前提が変わったので、こちらの設計も変える必要が出てきました。1 本の会話にどこまで積めるのか、そしてどこで切るのか。手元で計測してみると、直感で決めていた切り方が間違っていたことが分かりました。

「1 メッセージ 1 ターン」が意味すること

--input-format stream-json は、標準入力の 1 行を 1 通のユーザーメッセージとして受け取ります。行の形は以下です。

{"type":"user","message":{"role":"user","content":[{"type":"text","text":"アプリ wallpaper-a の ja 向けリリースノートを4行以内でまとめてください"}]}}

呼び出しはこうなります。

agy --print \
    --input-format stream-json \
    --output-format stream-json \
    < jobs.ndjson

ここで押さえておきたいのは、会話が閉じないぶん、履歴は積み上がり続けることです。2 件目のターンは 1 件目のやり取りを、24 件目のターンは 23 件分を抱えています。用語が揃うのはこの履歴のおかげであり、遅くなるのも同じ理由です。

つまり --input-format stream-json が与えてくれたのは「便利な入力形式」ではなく、会話の寿命を呼び出し側が決める権利でした。権利を受け取った以上、決め方を用意しなければなりません。

外部ドライバの最小形

まず、1 ターンの終端を待ってから次を送るドライバを書きます。送りっぱなしにしない、という 1 点だけで挙動が安定します。

// stream-driver.mjs — 外部ドライバから CLI を stream-json で回す
import { spawn } from "node:child_process";
import readline from "node:readline";
 
export async function runBatch(jobs, opts = {}) {
  const cmd = opts.cmd ?? ["agy", "--print", "--input-format", "stream-json", "--output-format", "stream-json"];
  const budget = opts.budgetBytes ?? 40_000; // 1 会話が抱える上限
  const stats = { sessions: 0, turns: 0, peakCarried: 0, splits: [] };
 
  let child = null, rl = null, carried = 0;
 
  const open = () => {
    child = spawn(cmd[0], cmd.slice(1), { stdio: ["pipe", "pipe", "inherit"] });
    rl = readline.createInterface({ input: child.stdout }); // 行境界は readline に任せる
    carried = 0;
    stats.sessions += 1;
  };
  const close = () => { child?.stdin.end(); rl?.close(); child = null; };
 
  // 1 メッセージ送って、そのターンの終端イベントが返るまで待つ
  const sendAndWait = (text) =>
    new Promise((resolve, reject) => {
      const onLine = (line) => {
        let ev;
        try { ev = JSON.parse(line); } catch { return; } // 部分行・装飾行は捨てる
        if (ev.type === "result") { rl.off("line", onLine); resolve(ev); }
        if (ev.type === "error") { rl.off("line", onLine); reject(new Error(ev.reason)); }
      };
      rl.on("line", onLine);
      child.stdin.write(
        JSON.stringify({ type: "user", message: { role: "user", content: [{ type: "text", text }] } }) + "\n"
      );
    });
 
  open();
  for (const job of jobs) {
    const size = Buffer.byteLength(job, "utf8");
    if (carried + size > budget && stats.turns > 0) {
      stats.splits.push({ afterTurn: stats.turns, carried });
      close(); open();                    // 会話を畳んで開き直す
    }
    const ev = await sendAndWait(job);
    carried = ev.carried_bytes ?? carried + size;
    stats.turns += 1;
    stats.peakCarried = Math.max(stats.peakCarried, carried);
  }
  close();
  return stats;
}

readline を挟んでいるのは趣味ではありません。子プロセスの stdout が届く単位は行の境界と一致しないためです。data イベントの中身をそのまま JSON.parse に渡す実装は、たまたま行が揃っているうちは動き、出力が長くなった瞬間に壊れます。手元で少数のジョブを試している間は表に出ず、本番のバッチで件数が増えてから顔を出す種類の落とし穴です。行の組み立てを最初から専用の口へ任せておけば、この経路はまるごと回避できます。

carried_bytes は、その会話がそれまでに受け取った入力の累計です。実物の CLI が同じ名前で返すとは限らないので、返らない場合はドライバ側の加算にフォールバックしています。

ここまでお読みいただきありがとうございます。

この記事の続きを読む

この先には、実装コードやベンチマーク結果など、実務でお役に立てる内容をご用意しています。このサイトは広告を掲載しておらず、サーバーや開発にかかる費用はメンバーの皆様のご支援で成り立っています。もしお役に立てていましたら、ご支援いただけますと大変ありがたいです。

この記事で得られること
長いバッチをエージェントへ流すとき、会話を1本で通すか分けるかを、後から作り直さずに済む基準で決められるようになる
累積バイト数だけで会話を切ると、表記を揃えたい仕事の途中で文脈が切れることを、実装前に避けられるようになる
24 件のジョブで累積が 43,854 バイトまで伸びる様子を確かめた計測手順ごと持ち帰り、自分のバッチで同じ数字を出せるようになる
Stripe による安全な決済 · いつでもキャンセル可能

この記事を購入する

この先の内容をすべてお読みいただけます。一度のご購入で、いつでも何度でもアクセスできます。このサイトは広告を掲載しておらず、皆さまのご支援がサーバー費用などの運営を支えています。

または
メンバーシップなら全記事が読み放題 →
シェア

お読みいただきありがとうございます

Antigravity Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • コピー&ペーストで使える実装コード付き
  • 毎日新しい上級ガイドを追加
  • ¥580/月 または ¥2,480 の永久アクセス
メンバーシップを見る →

関連記事

連携・プラグイン2026-08-20
MCP サーバーが1本も起動しない原因は、起動前の 40 行で特定できます
設定ファイルの1件の書き間違いで MCP サーバーが揃って起動しなくなる状況を、起動前に検査する小さなスクリプトで潰す手順です。実際に壊した設定へ通した出力と、JSON の同名キーが黙って消える落とし穴まで扱います。
連携・プラグイン2026-07-19
無応答の MCP サーバーでエージェントが止まる — 接続・一覧・呼び出しに別々のタイムアウトを敷く
Antigravity CLI 1.1.3 は、無応答の MCP サーバーがエージェントを無限に止める問題を、接続・ツール一覧・ツール呼び出しごとのタイムアウトで塞ぎました。3つの境界で失敗の出方が違う理由を整理し、防御ラッパーとサーキットブレーカー、失敗時だけの通知を、動くコードと夜間運用の実測でまとめます。
連携・プラグイン2026-07-18
黙って通していた確認ツールを、ソフト拒否から allow ルールに起こす
Antigravity CLI 1.1.3 で、ヘッドレスの -p は確認の要るツールを黙って自動承認する代わりに、ソフト拒否して必要な allow ルール名を stderr に示すようになりました。この出力を「発見」に使い、全許可からではなく空許可から最小権限を組み上げる逆向きのループを、動くハーネスと個人運用の実測でまとめます。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →