ENGINEERING GUIDE / 2026.09.22
BUILD WITH CODEX · HANDS-ON 01

Codex App Server通信を追って、エージェントの UI をつくる。

送るのは、1 行の JSON。返ってくるのは、実行の過程。会話・進捗・承認を自分のアプリに組み込むための仕組みを、小さなクライアントから学びます。

目安 70〜100 分JavaScript 基礎図解 6 点 + 実行サンプル
01 / ORIENTATION

自分の UI と、Codex の実行をつなぐ

App Server は、Codex をアプリへ深く組み込むためのインターフェースです。クライアントが会話の開始やユーザー入力を送り、サーバーがエージェントの進捗や承認要求を返します。たとえば、チャット画面の横に「実行中のコマンド」と「承認ボタン」を表示するために使えます。

このガイドの到達点
初期化を済ませ、1 ターンを送信し、ストリーミングと最終状態を受け取る Node.js クライアントを動かす。さらに、承認・再開・中断をどこに組み込むか説明できる。
あなたのクライアントNode.js / UICodex App Server会話・実行・承認モデル / ツール推論・作業JSONL実行stdin へ要求を送るstdout へ通知や要求を返すモデル呼び出し時は外部通信
図 1 stdio 接続の担当範囲。右側の破線はネットワークを含む実行先との境界

この教材では、Node.js が子プロセスとして codex app-server を起動します。画面上のデモはブラウザ内だけで動くシミュレーションです。公開サイトから手元の Codex を操作することはありません。

やりたいことまず検討する手段扱う範囲
会話履歴・承認・進捗を独自 UI に統合App Server接続と状態を持つ双方向クライアント
ジョブや CI をプログラムから自動化Codex SDK公式資料が案内する自動化の入口
CLI から単発の処理を実行codex execコマンドとしての非対話実行
Codex に外部ツールやデータを提供MCPエージェントが使う機能の拡張

App Server と MCP は、ともに JSON-RPC を使いますが、目的とメソッドは異なります。App Server の要求を、そのまま MCP サーバーに送ることはできません。

一次情報:App Server — 概要・Protocol / Codex SDK / Non-interactive mode / MCP

02 / MENTAL MODEL

Thread → Turn → Item で考える

Thread は会話のまとまり、Turn は 1 回のユーザー入力と、それに続くエージェントの作業、Item はその中の個々の入出力です。1 つの返答を「文字列 1 本」として扱うより、途中経過を UI に表示しやすくなります。

Thread会話のまとまり例:設計を相談する会話Turn1 回の入力と続く作業例:構成案を説明してItem個別の入出力例:入力・返答・実行結果1 : 多1 : 多rpc id は通信の照合用。threadId / turnId / itemId は作業の識別用。
図 2 会話を構成する 3 つの単位と、その関係
識別子何を特定するか例
idJSON-RPC の要求と応答3
threadIdどの会話かthr_demo
turnIdどの入力に対する作業かturn_1
itemIdどのメッセージ・コマンド等かmsg_1

この表の値は説明用です。実接続では応答から受け取った値を使います。id: 3 の応答が返っても、ターンが終わったとは限りません。作業の終了は turn/completed の通知で判断します。

一次情報:Core primitives / Lifecycle overview

03 / PROTOCOL LAB

1 メッセージずつ、通信を進める

stdio 接続では、JSON オブジェクト 1 個を 1 行として送り、末尾に改行を付けます。一般的な JSON-RPC 2.0 の形式に基づきますが、通信上は "jsonrpc":"2.0" を省略します。複数行に整形した JSON をそのまま送ると、JSONL の区切りを壊します。

REQUEST / RESPONSE

method + id は要求。応答は同じ id に result または error を付けて返します。

NOTIFICATION

method があり、id がないメッセージは通知。通知に応答を返す必要はありません。

ClientApp Serverinitialize · id: 1result · id: 1initialized · id なしthread/start · id: 2result.thread.id · id: 2turn/start · id: 3result.turn.id · id: 3item/agentMessage/deltaturn/completed
図 3 代表的な通信の順序。実線は要求と応答、破線は通知。追加通知は省略

通信シミュレーター

外部通信・モデル呼び出しなし。メッセージは学習用に抜粋しています。実際には追加の通知が届き、応答と通知の到着順が前後することもあります。

接続初期化スレッド実行完了

LAB 1 · 5 分

受付と完了の間を観察する

  1. 「初期化を飛ばすと?」を押し、エラーの理由を確かめる。
  2. 最初から順に進め、turn/start の応答で止める。
  3. まだ文章が完成していないことを確認し、turn/completed まで進める。

一次情報:Protocol / Message schema / Initialization

04 / PREPARE

実習用フォルダーを用意する

ターミナル操作と JavaScript の async/await が分かれば進められます。実習コードは Node.js 標準モジュールのみで動きます。Node.js 22 以上を推奨し、教材作成時は Node.js 26.8.2 と Codex CLI 0.154.0 で確認しています。

最初はモックから。 LAB 2 は Codex のインストール・認証なしで実行できます。LAB 3 の初期化確認ではモデルを呼び出しません。LAB 4 の --live は実際のモデルに入力を送り、利用枠または API 料金を消費します。
ターミナル · 練習用フォルダー
mkdir -p ~/codex-app-server-lab
cd ~/codex-app-server-lab
node --version

次の ZIP をダウンロードして展開し、client.mjs、rpc.mjs、mock-server.mjs をこのフォルダーへ置きます。ZIP 内の README にも実行コマンドがあります。

実サーバーを使う段階で実施
# まだ Codex CLI がない場合
npm install -g @openai/codex

# インストール済みなら、まずバージョンとオプションを確認
codex --version
codex app-server --help

# 実モデルを使う場合はログイン状態を確認
codex login status
# 未認証の場合のみ実行し、表示される案内に従う
codex login

インストールされる最新版と本教材の検証版は一致しない場合があります。差異があるときは、手元のバイナリからスキーマを生成して確かめます。既存の認証情報を HTML やソースコードに貼り付ける必要はありません。

手元のバージョンに一致する仕様を出力
codex app-server generate-ts --out ./schemas-ts
codex app-server generate-json-schema --out ./schemas-json

公式 Web ドキュメントは更新され、手元の環境と差異が生じる場合があります。一方、生成物は実行した Codex の版に対応します。フィールド名・必須項目・列挙値に迷ったら、生成された v2/TurnStartParams.json などを読むと、実装とのずれを減らせます。

一次情報:Codex CLI / Authentication / スキーマ生成

05 / FIRST CONNECTION

モックで動かし、本物に接続する

LAB 2 · 5 分 · モック

ネットワークなしで 1 ターンを完走する

node client.mjs

おおむね次の結果が表示されます。モックは、通知を取りこぼさないか確かめるため、turn/start の応答より先に完了通知を送ります。

[ready] initialize → initialized
[thread] thr_demo
接続できました。これはモック応答です。
[turn] turn_1: completed
[final] 接続できました。これはモック応答です。

本物への接続でも、最初の操作は同じです。initialize の応答を待ち、initialized を通知してから、他のメソッドを使います。初期化は接続ごとに 1 回です。

client.mjs の核心 · 接続後の順序
await rpc.request('initialize', {
  clientInfo: {
    name: 'app_server_handson',
    title: 'App Server Hands-on',
    version: '1.0.0'
  }
});
rpc.notify('initialized');
// この後で thread/start などを送る
LAB 3 · 5 分 · 実サーバー / モデル呼び出しなし

初期化だけを確認する

node client.mjs --handshake

[ready] initialize → initialized が表示され、プロセスが終了すれば成功です。接続・JSONL・初期化までを確かめた状態であり、認証やモデル利用の成功までは示しません。

実サーバーは既存の Codex 設定を読み込みます。起動時の管理設定取得など、モデル推論以外の通信が発生する場合があります。

実習には、機密ファイルを含まない練習用フォルダーを使ってください。

一次情報:Getting started / Initialization

06 / RUN A TURN

会話を作り、最初の入力を送る

thread/start は会話の入れ物を作ります。ここでは cwd を練習用フォルダーに固定し、ファイル操作を読み取り専用に制限します。approvalPolicy: 'never' は「承認を求めない」という意味で、サンドボックスを解除する設定ではありません。

client.mjs の抜粋 · Thread と Turn
const { thread } = await rpc.request('thread/start', {
  cwd: process.cwd(),
  sandbox: 'read-only',
  approvalPolicy: 'never'
});

const { turn } = await rpc.request('turn/start', {
  threadId: thread.id,
  input: [{
    type: 'text',
    text: 'ツールを使わず、日本語で「接続できました」とだけ返してください。',
    text_elements: []
  }]
});
// turn.id を保存。ここでは作業完了を判断しない。

モデル名は固定せず、Codex の既存設定を使います。明示的に選ぶ場合は model/list で利用候補を確認し、実際に使う ID を指定します。プランや組織の設定によって利用可能なモデルは異なります。

LAB 4 · 10 分 · 実モデル / 利用枠・料金あり

短い返答を受け取る

node client.mjs --live
  1. [thread] に続く ID を控える。
  2. テキストが届き、[turn] ...: completed が出るまで待つ。
  3. [final] の確定メッセージを確認する。モデルの出力や分割数は実行ごとに変わります。

利用枠を消費したくない場合は、この実習をスキップして LAB 5 に進めます。モックで検証できるのはクライアントの処理であり、実サービスの認証や応答ではありません。

client.mjs 全文を読む
import { spawn } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import { RpcClient } from './rpc.mjs';

const args = process.argv.slice(2);
const live = args.includes('--live');
const handshake = args.includes('--handshake');
if (live && handshake) throw new Error('--live と --handshake は同時に使えません');
const resumeAt = args.indexOf('--resume');
const resumeId = resumeAt >= 0 ? args[resumeAt + 1] : null;
if (resumeAt >= 0 && (!resumeId || resumeId.startsWith('--'))) throw new Error('--resume に threadId が必要です');
const proc = live || handshake
  ? spawn('codex', ['app-server'], { stdio: ['pipe', 'pipe', 'inherit'] })
  : spawn(process.execPath, [fileURLToPath(new URL('./mock-server.mjs', import.meta.url))], {
      stdio: ['pipe', 'pipe', 'inherit'],
      env: { ...process.env, GUIDE_MOCK_APPROVAL: args.includes('--approval') ? '1' : '0' }
    });
const completed = new Map();
const messages = new Map();
const key = (threadId, turnId) => JSON.stringify([threadId, turnId]);
let rpc;
let deadline;
try {
  rpc = new RpcClient(proc, {
    onServerRequest: m => {
      if (['item/commandExecution/requestApproval', 'item/fileChange/requestApproval'].includes(m.method)) {
        console.log(`[approval] ${m.method} → decline(教材の既定値)`);
        return { decision: 'decline' };
      }
      // 未対応要求を許可したことにしない。RpcClient が -32601 を返す。
    },
    onNotification: m => {
      const p = m.params;
      if (m.method === 'item/agentMessage/delta') {
        const k = JSON.stringify([p.threadId, p.turnId, p.itemId]);
        messages.set(k, (messages.get(k) ?? '') + p.delta);
        process.stdout.write(p.delta);
      }
      if (m.method === 'item/completed' && p.item?.type === 'agentMessage') {
        const k = JSON.stringify([p.threadId, p.turnId, p.item.id]);
        messages.set(k, p.item.text); // 確定内容で置換。delta に追記しない。
      }
      if (m.method === 'turn/completed') completed.set(key(p.threadId, p.turn.id), p.turn);
    }
  });
  await rpc.request('initialize', { clientInfo: { name: 'app_server_handson', title: 'App Server Hands-on', version: '1.0.0' } });
  rpc.notify('initialized');
  console.log('[ready] initialize → initialized');
  if (!handshake) {
    const params = { cwd: process.cwd(), sandbox: 'read-only', approvalPolicy: 'never' };
    const result = resumeId
      ? await rpc.request('thread/resume', { ...params, threadId: resumeId })
      : await rpc.request('thread/start', params);
    const threadId = result.thread.id;
    console.log(`[thread] ${threadId}`);
    const { turn } = await rpc.request('turn/start', {
      threadId,
      input: [{ type: 'text', text: 'ツールを使わず、日本語で「接続できました」とだけ返してください。', text_elements: [] }]
    });
    const until = Date.now() + 120000;
    while (!completed.has(key(threadId, turn.id))) {
      if (rpc.closed) throw new Error('完了通知の前に接続が終了しました');
      if (Date.now() > until) {
        // タイムアウトは完了ではない。停止を要求し、成功と表示しない。
        await rpc.request('turn/interrupt', { threadId, turnId: turn.id }).catch(() => {});
        throw new Error('turn timeout: 停止要求を送信しました。状態を確認してください');
      }
      await new Promise(resolve => { deadline = setTimeout(resolve, 20); });
    }
    const final = completed.get(key(threadId, turn.id));
    console.log(`\n[turn] ${final.id}: ${final.status}`);
    console.log('[final]', [...messages.values()].join('\n'));
    if (final.status !== 'completed') throw new Error(`turn ${final.status}: ${final.error?.message ?? ''}`);
  }
} catch (error) {
  console.error(error.message);
  process.exitCode = 1;
} finally {
  clearTimeout(deadline);
  rpc?.close();
  if (!rpc) proc.kill();
}

一次情報:API overview — thread/start・turn/start・model/list / Sandbox と approvals

07 / EVENT ROUTING

届いた JSON を、3 方向に振り分ける

接続中は常に受信を待ち続けます。サーバーはクライアントに要求を送ることもあるため、id だけを見て「自分の要求への応答」と判断してはいけません。まず method の有無を確認します。

method あり + id ありサーバーからの要求例:承認。対象を表示して判断を返すmethod あり + id なし通知例:delta。該当 Item の表示を更新method なし + id あり応答result / error で待機中の要求を解決
図 4 受信メッセージの判別。両方向の要求 ID を取り違えない
受信分類の要点
const hasId = Object.hasOwn(message, 'id');
if (message.method && hasId) {
  // サーバーからの要求。ユーザーの判断などを返す。
} else if (message.method) {
  // 通知。画面やターン状態を更新する。
} else if (hasId) {
  // 応答。pending.get(message.id) を解決する。
}

if (message.id) では ID が 0 の要求を見落とします。また、リクエストと応答の到着順が同じとは限らないため、待機中の要求は Map で管理します。要求送信前から受信処理を登録しておくことも重要です。

delta は追記、completed は確定

通知UI の更新注意点
item/startedメッセージや作業の表示枠を用意テキスト以外の Item もある
item/agentMessage/delta該当 Item に文字列を追記会話・ターン・Item の ID で分離する
item/completed確定した Item で表示を置換確定テキストの再追記は二重表示になる
turn/completedターンの処理中表示を終了status を確認。成功とは限らない

教材では threadId + turnId + itemId をキーに本文を管理し、threadId + turnId をキーに完了通知を保存します。これにより、開始応答を処理する前に届いた完了通知も拾えます。画面へ表示するときは textContent などを使い、受信文字列をそのまま HTML として挿入しません。

完成扱いにする条件を明確に。 turn/completed は終了を表す通知です。status が completed、failed、interrupted のどれかを見て、成功・失敗・中断を分けて表示します。
rpc.mjs 全文を読む
import readline from 'node:readline';

// 受信順に依存せず、応答・通知・サーバー要求を振り分ける。
export class RpcClient {
  constructor(proc, { onNotification = () => {}, onServerRequest, timeoutMs = 15000 } = {}) {
    this.proc = proc;
    this.pending = new Map();
    this.nextId = 1;
    this.timeoutMs = timeoutMs;
    this.closed = false;
    this.rl = readline.createInterface({ input: proc.stdout });
    this.rl.on('line', line => {
      try {
        const m = JSON.parse(line);
        const hasId = Object.hasOwn(m, 'id');
        if (m.method && hasId) {
          // 両方向の id は別物。同じ値でも pending の応答として扱わない。
          Promise.resolve().then(() => onServerRequest?.(m)).then(result => {
            this.send(result === undefined
              ? { id: m.id, error: { code: -32601, message: 'Unsupported server request' } }
              : { id: m.id, result });
          }).catch(() => {
            if (!this.closed) this.send({ id: m.id, error: { code: -32603, message: 'Client handler failed' } });
          });
        } else if (m.method) {
          onNotification(m);
        } else if (hasId) {
          const p = this.pending.get(m.id);
          if (!p) return;
          clearTimeout(p.timer);
          this.pending.delete(m.id);
          m.error ? p.reject(new Error(`${m.error.code}: ${m.error.message}`)) : p.resolve(m.result);
        }
      } catch (error) { this.fail(error); }
    });
    proc.on('error', error => this.fail(error));
    proc.on('exit', (code, signal) => this.fail(new Error(`server exit: ${code ?? signal}`)));
    proc.stdin.on('error', error => this.fail(error));
    proc.stdout.on('error', error => this.fail(error));
  }
  send(message) {
    if (this.closed) throw new Error('connection closed');
    this.proc.stdin.write(JSON.stringify(message) + '\n');
  }
  request(method, params = {}) {
    if (this.closed) return Promise.reject(new Error('connection closed'));
    const id = this.nextId++;
    return new Promise((resolve, reject) => {
      const timer = setTimeout(() => {
        this.pending.delete(id);
        reject(new Error(`timeout: ${method}; 結果不明のため自動再送しません`));
      }, this.timeoutMs);
      this.pending.set(id, { resolve, reject, timer });
      try { this.send({ id, method, params }); }
      catch (error) { clearTimeout(timer); this.pending.delete(id); reject(error); }
    });
  }
  notify(method, params = {}) { this.send({ method, params }); }
  fail(error) {
    this.closed = true;
    for (const p of this.pending.values()) { clearTimeout(p.timer); p.reject(error); }
    this.pending.clear();
  }
  close() {
    this.fail(new Error('client closed'));
    this.rl.close();
    this.proc.stdin.end();
    this.proc.kill();
  }
}
モックの実装と、その限界

モックは初期化、開始・再開、テキスト通知、承認の一部だけを再現します。実サーバーの完全なスキーマや実行環境を代替するものではありません。

import readline from 'node:readline';
const send = m => process.stdout.write(JSON.stringify(m) + '\n');
let initialized = false;
let acknowledged = false;
let approvalId;
let turn = 0;
const finish = () => {
  const ids = { threadId: 'thr_demo', turnId: `turn_${turn}`, itemId: `msg_${turn}` };
  send({ method: 'item/started', params: { ...ids, item: { id: ids.itemId, type: 'agentMessage', text: '' } } });
  for (const delta of ['接続できました。', 'これはモック応答です。']) {
    send({ method: 'item/agentMessage/delta', params: { ...ids, delta } });
  }
  send({ method: 'item/completed', params: { ...ids, item: { id: ids.itemId, type: 'agentMessage', text: '接続できました。これはモック応答です。' } } });
  send({ method: 'turn/completed', params: { threadId: ids.threadId, turn: { id: ids.turnId, status: 'completed', items: [], error: null } } });
};
readline.createInterface({ input: process.stdin }).on('line', line => {
  const m = JSON.parse(line);
  if (!m.method) {
    if (m.id === approvalId) {
      send({ method: 'serverRequest/resolved', params: { threadId: 'thr_demo', requestId: m.id } });
      finish();
    }
    return;
  }
  if (m.method === 'initialize') {
    if (initialized) return send({ id: m.id, error: { code: -32600, message: 'Already initialized' } });
    initialized = true;
    return send({ id: m.id, result: { userAgent: 'guide-mock/1.0', platformFamily: 'unix', platformOs: 'mock' } });
  }
  if (m.method === 'initialized') { acknowledged = initialized; return; }
  if (!acknowledged) return send({ id: m.id, error: { code: -32600, message: 'Not initialized' } });
  if (m.method === 'thread/start' || m.method === 'thread/resume') {
    return send({ id: m.id, result: { thread: { id: 'thr_demo' } } });
  }
  if (m.method === 'turn/start') {
    turn++;
    send({ method: 'turn/started', params: { threadId: 'thr_demo', turn: { id: `turn_${turn}`, status: 'inProgress', items: [], error: null } } });
    if (process.env.GUIDE_MOCK_APPROVAL === '1') {
      approvalId = m.id; // 同じ id の逆向き要求でも衝突しないことを試す。
      send({ method: 'item/commandExecution/requestApproval', id: approvalId, params: { threadId: 'thr_demo', turnId: `turn_${turn}`, itemId: 'cmd_demo', command: 'echo demo', cwd: '/tmp', reason: '教材の模擬承認' } });
    } else finish(); // 応答より先に完了通知を送り、取りこぼしを試す。
    send({ id: m.id, result: { turn: { id: `turn_${turn}`, status: 'inProgress', items: [], error: null } } });
    return;
  }
  send({ id: m.id, error: { code: -32601, message: 'Method not found' } });
});

一次情報:Events / Approvals。Map による管理と描画方法は本教材の実装例です。

08 / HUMAN IN THE LOOP

承認では、サーバーが質問する側になる

設定によって、コマンド実行やファイル変更に承認が必要になります。このときサーバーから method + id を持つ要求が届きます。クライアントは対象操作をユーザーに示し、サーバーが付けた ID をそのまま使って応答します。

Client / 承認 UIApp Serveritem/started(対象 Item)requestApproval · id: 81result.decision · id: 81serverRequest/resolveditem/completed
図 5 承認時の通信。要求の向きが反転する。破線は通知
サーバー → クライアント · 説明用の要求
{
  "id": 81,
  "method": "item/commandExecution/requestApproval",
  "params": {
    "threadId": "thr_demo", "turnId": "turn_1",
    "itemId": "cmd_demo", "command": "echo demo",
    "cwd": "/tmp", "reason": "教材の模擬承認"
  }
}
クライアント → サーバー · 拒否の応答
{ "id": 81, "result": { "decision": "decline" } }

accept は承認、decline は拒否です。acceptForSession など追加の選択肢もありますが、権限の範囲を広げる判断を UI が勝手に行ってはいけません。availableDecisions が届く場合は、その選択肢と手元のスキーマに従います。未対応のサーバー要求に空の成功応答を返すことも避けます。

LAB 5 · 5 分 · モック

逆向きの要求を受け取る

node client.mjs --approval

[approval] ... → decline を確認します。モックは要求と逆向き要求に同じ ID を使います。受信処理が method を先に判定していれば、取り違えずに応答できます。

このモックでは拒否した後も模擬テキストを返します。実サーバーで承認を拒否した後のエージェントの行動や終了状態は、作業内容によって変わります。

UI に組み込むときの表示項目

対象の会話・ターン・Item に加え、コマンド、作業ディレクトリ、理由を確認できるようにします。ファイル変更なら変更内容を示します。ネットワーク承認の networkApprovalContext がある場合は、宛先ホスト・プロトコル等を示し、通常のコマンド承認と区別します。別クライアント等で解決された要求は serverRequest/resolved で画面から閉じます。

一次情報:Approvals — コマンド・ファイル・ネットワーク承認

09 / CONTINUE & CANCEL

「続きを送る」と「今の作業に追加する」を分ける

意図メソッド必要な考え方
保存した会話を再開thread/resume返された Thread に次の Turn を送る
履歴を読むだけthread/read読む操作だけでは会話を再開しない
進行中の入力へ補足turn/steer対象の expectedTurnId が一致する必要がある
進行中の作業を中断turn/interrupt応答後も turn/completed を待つ
履歴から別会話に分岐thread/fork新しい Thread ID を持つ会話を作る
LAB 6 · 10 分 · 再開

同じ Thread にもう 1 ターン送る

モックで引数と呼び出し経路を試す場合:

node client.mjs --resume thr_demo

実際の保存と再開を確認する場合は、LAB 4 で控えた ID に置き換えて実行します。この操作も実モデルを呼び出します。

node client.mjs --live --resume <控えたthreadId>

実接続では同じ Thread ID と、新しい Turn ID が表示されることを確認します。モックの履歴は保存されず、毎回固定 ID を返すため、永続化の検証にはなりません。

進行中のターンを操作する

以下はクライアント内で使う追加実装の例です。threadId と turnId には対象の実 ID を渡し、ターンが進行中のときだけ実行します。

// 今のターンへ補足。新しい turn/started は発生しない。
await rpc.request('turn/steer', {
  threadId,
  expectedTurnId: turnId,
  input: [{ type: 'text', text: '説明を3項目にまとめてください。', text_elements: [] }]
});

// 停止要求。応答の {} を作業完了と解釈しない。
await rpc.request('turn/interrupt', { threadId, turnId });
// 受信を続け、対象ターンの turn/completed と status を確認する。

中断は、すでに実行された副作用を巻き戻す操作ではありません。また、接続が切れたこととターンが終わったことは別です。再接続時は初期化をやり直し、保存済み履歴・状態を確認してから画面を復元します。ターン作成を無条件に再送すると、作業が重複する可能性があります。

一次情報:thread/resume・thread/read・thread/fork / Steer an active turn / Interrupt a turn

10 / TROUBLESHOOTING

止まった場所から、原因を絞る

症状最初に確認すること対処
spawn codex ENOENT同じターミナルで codex --version が動くかCLI のインストールと PATH を確認
Not initialized接続後に初期化を済ませたかinitialize の応答 → initialized → 通常の要求
Already initialized同じ接続へ initialize を 2 回送っていないか接続単位で初期化状態を管理
JSON の解析に失敗stdout にデバッグ文字列を混ぜていないかJSONL は stdout、診断ログは stderr に分離
開始したまま止まるサーバーからの承認要求を無視していないかmethod + id のメッセージを処理
同じ文章が 2 回表示delta と確定全文を両方追記していないかitem/completed は確定値で置換
認証・モデル・利用枠のエラー接続成功と実行成功を混同していないかログイン状態、model/list、エラー情報を確認
フィールドが拒否されるCLI の版とドキュメントが一致するかスキーマを生成。実験 API の opt-in も確認
WebSocket の -32001要求が集中していないか指数バックオフとジッターで再試行を調整

通信方式を増やす前に

stdio は子プロセスと組み合わせやすい入口です。WebSocket は 1 テキストフレームに 1 メッセージを載せます。Unix socket の接続も単なる JSON の直書きではなく、WebSocket の HTTP Upgrade ハンドシェイクを使います。

公開する対象を取り違えない。 このサイトで公開しているのは静的な教材です。App Server 自体を Cloudflare に配備しているわけではありません。公式資料は App Server コマンドと WebSocket transport を experimental と説明し、本番ワークロードのサポート対象外としています。

リモート接続に進む場合は、公式資料の認証フラグと TLS の手順を確認します。平文の ws:// は localhost または SSH 転送で使い、ネットワークへ公開する際は認証を設定します。本教材の HTML にトークンを埋め込んだり、ローカルの App Server をそのまま公開したりする構成は採りません。

一次情報:Protocol — transport・認証・過負荷 / Experimental API opt-in / 実験機能とサポート範囲

11 / CHECK YOUR UNDERSTANDING

実装の判断ができるか確かめる

各問で答えを選ぶと、理由を表示します。理解が曖昧なら対応する章へ戻り、通信をもう一度追ってください。

Q1. turn/start の result が返った。画面は?

Q2. method と id: 0 を持つメッセージが届いた。

Q3. delta の後に item/completed の全文が届いた。

Q4. 実行中の同じターンに補足を入れたい。

発展課題:最小クライアントから自分の UI へ
  1. 複数の Item を個別の行に表示し、進行中・完了・失敗を区別する。
  2. 承認要求をキューで管理し、対象と理由を表示してから判断を返す。
  3. 切断時は「状態未確認」と表示し、再接続後に履歴と照合する。
  4. UI から停止要求を送り、対象ターンの終了通知まで観察する。

完成条件は、途中経過が見えること、拒否や失敗も表示できること、同じ作業を勝手に重複実行しないことです。

12 / SOURCES & SCOPE

一次情報と、教材の検証範囲

仕様の説明は OpenAI の公式ドキュメントをもとに構成し、コード・図・シミュレーターは学習用に作成しました。公式サンプルの省略形と、手元のスキーマで必要な構造を区別しています。確認日:。

  1. Codex App Server 公式ガイド — 通信、初期化、Thread / Turn / Item、承認、認証、transport。
  2. openai/codex — App Server 実装 — 実装側の仕様を調べる入口。
  3. Codex CLI / Authentication — 導入と認証。
  4. Codex SDK / Non-interactive mode / MCP — 用途別の選択肢。
  5. StellarArc design-system — カラー、タイポグラフィ、文書レイアウト、inline SVG 規定。

検証済み:Codex CLI 0.154.0 のスキーマ生成、ローカル App Server の初期化、モックのターン処理・承認、受信処理のテスト、表示と操作のブラウザ検証。

実モデルの呼び出し:教材作成時は未実施です。LAB 4 と実接続の LAB 6 は、利用者が自身の認証・利用枠で実施する手順として掲載しています。

表現レビュー:Gemini 3.8 Flash High によるレビュー後、仕様・識別子・コードへの影響を確認して表現を調整しています。

ブラウザの実習チェックは localStorage に保存します。外部への送信は行いません。Web フォントの読み込みには Google Fonts を使い、通信できない場合はローカル書体へ切り替わります。