OpenAI Agents API入門——FirestoreでAIセッション台帳を作る
OpenAI Agents API公開ベータを題材に、Next.js + Firestore + GitHub Actionsで長時間AIタスクのセッション台帳を作る実践手順を解説します。

はじめに
AIエージェントを業務アプリに組み込むと、単発のテキスト生成だけでは足りない場面が増えてきます。
| よくある状況 | 困ること |
|---|---|
| GitHub Issueを読み、調査方針まで作らせたい | 1リクエストで終わらず、途中経過を追いたい |
| 問い合わせや障害報告を20〜30件まとめて整理したい | どのAI作業が完了したか台帳が必要 |
| 長い調査を人があとから確認したい | セッションID、イベント、結果を業務データと結びたい |
OpenAI APIのChangelogでは、2026年9月10日に Agents APIがpublic betaとして公開 されたと案内されています。OpenAIがmanaged Codex harness、セッションのオーケストレーション、コンテキスト圧縮、復旧を担い、アプリ側は周辺のUI、認可、ツール接続、保存設計に集中できる位置づけです。
本記事では、架空の開発支援SaaS「ReviewPort」を題材に、Next.js App Router + Firestore + TypeScript + GitHub Actionsで「Agents APIのセッション台帳」を作ります。この記事を読み終えると、AIエージェントの実行をただ呼び出すだけでなく、アプリの監査ログとして扱う設計ができるようになります。
参考にした一次情報は次の通りです。
- OpenAI API Changelog
- OpenAI API Agents overview
- Agents API overview
- Agents API quickstart
- OpenAI TypeScript API reference: Sessions
Agents APIで何が変わるか
OpenAI公式ドキュメントでは、新しいエージェントアプリを作る場合の開始点としてAgents APIが案内されています。Agents APIは、OpenAI管理のCodex harnessで長時間タスクを動かし、セッション状態や進行イベントを扱えるのが特徴です。
似た名前の選択肢が複数あるので、まず役割を分けます。
| 選択肢 | 使いどころ | アプリ側が持つ責務 |
|---|---|---|
| Agents API | OpenAI管理の実行基盤で長時間タスクを任せたい | UI、認可、業務データ、ツール連携 |
| Agents SDK | 自分のアプリ内でエージェントループを制御したい | 実行、保存、承認、ツール実装 |
| Responses API | モデル呼び出しを直接制御したい | 会話履歴、ツール実行、再試行設計 |
Agents APIの中心概念は、Agent、Environment、Session、Events and itemsです。特にWebアプリで重要なのはSessionです。セッションは、AIがタスクに取り組むための永続的な単位で、アプリ側のプロジェクト、Issue、担当者、権限とひも付けて管理します。
事前準備
公式quickstartでは、JavaScript SDKとして openai パッケージを使い、SDK例では beta.agents namespaceを使います。リクエストには OpenAI-Beta: agents=v1 ヘッダーが必要ですが、OpenAI SDKを使う場合は自動で付与されると説明されています。
ReviewPortでは、Next.jsのサーバー側だけでOpenAI APIキーを扱います。クライアントコンポーネントにキーを渡さない構成にします。
yarn add openai firebase-admin
必要な権限も最小化します。公式quickstartでは、セッション操作に api.agents.read と api.agents.write、モデル推論に api.responses.write を付与する案内があります。まずは開発用プロジェクトで試し、本番ではプロジェクト単位のAPIキーと監査ログを分けます。
ハンズオン1: Firestoreにセッション台帳を設計する
まず、アプリ側の台帳を決めます。OpenAIのセッションIDだけを保存するのではなく、「誰が、どの業務データに対して、何を頼んだか」を一緒に残します。
export type AgentSessionLedger = {
provider: "openai";
openaiSessionId: string | null;
projectId: string;
issueNumber: number;
requestedBy: string;
taskKind: "issue_triage" | "release_note_draft" | "incident_review";
status: "queued" | "running" | "completed" | "failed";
promptPreview: string;
createdAt: FirebaseFirestore.FieldValue;
updatedAt: FirebaseFirestore.FieldValue;
};
ポイントは、promptPreview を短く保つことです。Issue本文や問い合わせ内容には社内情報が含まれます。全文を二重保存するより、元データへの参照と短い要約だけを残し、アクセス制御はFirestore側で統一します。
ハンズオン2: Next.jsからAgents APIセッションを作る
次に、Issue調査用のRoute Handlerを作ります。ここではコード実行やファイル操作をさせないため、environment.type は none にします。公式quickstartでも、sandboxが不要な場合は environment.type を none にできると案内されています。
// src/app/api/projects/[projectId]/agent-sessions/route.ts
import OpenAI from "openai";
import { FieldValue } from "firebase-admin/firestore";
import { adminDb } from "@/lib/firebase/admin";
import { requireUser } from "@/lib/auth/requireUser";
import { assertCanReadProject } from "@/lib/projects/permissions";
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
type Params = {
params: Promise<{ projectId: string }>;
};
type RequestBody = {
issueNumber: number;
issueTitle: string;
issueBody: string;
};
export async function POST(request: Request, { params }: Params) {
const user = await requireUser(request);
const { projectId } = await params;
const body = (await request.json()) as RequestBody;
await assertCanReadProject(user.uid, projectId);
const prompt = [
"あなたはWeb系SaaSのシニアエンジニアです。",
"次のGitHub Issueを読み、調査観点、リスク、最初の3タスクを日本語で整理してください。",
`Issue #${body.issueNumber}: ${body.issueTitle}`,
body.issueBody.slice(0, 6000),
].join("\n\n");
const ledgerRef = await adminDb.collection("agentSessionLedgers").add({
provider: "openai",
openaiSessionId: null,
projectId,
issueNumber: body.issueNumber,
requestedBy: user.uid,
taskKind: "issue_triage",
status: "queued",
promptPreview: prompt.slice(0, 500),
createdAt: FieldValue.serverTimestamp(),
updatedAt: FieldValue.serverTimestamp(),
});
const session = await openai.beta.agents.sessions.create({
agent: {
model: "gpt-6-astra",
instructions:
"根拠をIssue本文から分けて示し、実装前に確認すべき不明点も列挙してください。",
},
environment: { type: "none" },
input: prompt,
});
await ledgerRef.update({
openaiSessionId: session.id,
status: "running",
updatedAt: FieldValue.serverTimestamp(),
});
return Response.json({
ledgerId: ledgerRef.id,
openaiSessionId: session.id,
});
}
このAPIは、AIの最終回答をその場で待ち続ける設計ではありません。画面には台帳IDを返し、進行状況はFirestoreを購読して表示します。長い調査タスクほど、UIとAI実行を分けた方が運用しやすくなります。
ハンズオン3: イベントをFirestoreへ同期する
Agents APIでは、セッションイベントのストリームを扱えます。TypeScript API referenceでは、client.beta.agents.sessions.events.stream(sessionID) が案内されています。ReviewPortでは、GitHub Actionsから内部APIを定期実行し、動いているセッションのイベントを少しずつFirestoreへ取り込みます。
// src/app/api/internal/sync-agent-events/route.ts
import OpenAI from "openai";
import { FieldValue } from "firebase-admin/firestore";
import { adminDb } from "@/lib/firebase/admin";
import { assertCronRequest } from "@/lib/auth/assertCronRequest";
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
type AgentEventRecord = {
event_id?: string;
session_id?: string;
type: string;
};
export async function POST(request: Request) {
await assertCronRequest(request);
const snapshot = await adminDb
.collection("agentSessionLedgers")
.where("provider", "==", "openai")
.where("status", "==", "running")
.limit(5)
.get();
for (const doc of snapshot.docs) {
const data = doc.data() as { openaiSessionId?: string };
if (!data.openaiSessionId) continue;
const events = await openai.beta.agents.sessions.events.stream(
data.openaiSessionId,
);
let storedCount = 0;
try {
for await (const event of events) {
const record = event as AgentEventRecord;
const eventId = record.event_id ?? crypto.randomUUID();
await doc.ref.collection("events").doc(eventId).set(
{
type: record.type,
openaiSessionId: record.session_id ?? data.openaiSessionId,
receivedAt: FieldValue.serverTimestamp(),
},
{ merge: true },
);
await doc.ref.update({
lastEventType: record.type,
lastEventSyncedAt: FieldValue.serverTimestamp(),
updatedAt: FieldValue.serverTimestamp(),
});
storedCount += 1;
if (storedCount >= 20) {
break;
}
}
} finally {
events.controller.abort();
}
}
return Response.json({ ok: true });
}
イベントのpayload全文を保存するかは、組織の監査ポリシーに合わせて決めます。まずは type、event_id、session_id、受信時刻だけに絞ると、Firestoreの肥大化と機密情報の重複保存を抑えられます。完了や失敗の判定を自動化する場合は、利用しているOpenAI SDKの AgentSessionEvent 型でイベント名を確認し、プロジェクト側で固定してから分岐させます。
ハンズオン4: GitHub Actionsで同期を回す
内部APIを5分ごとに呼び出します。大量のセッションを一度に処理せず、1回あたり5件程度に絞ると、失敗時の再実行も追いやすくなります。
name: Sync OpenAI agent sessions
on:
schedule:
- cron: "*/5 * * * *"
workflow_dispatch:
jobs:
sync:
runs-on: ubuntu-latest
steps:
- name: Sync running agent sessions
run: |
curl --fail --request POST "${APP_URL}/api/internal/sync-agent-events" \
--header "Authorization: Bearer ${CRON_SECRET}"
env:
APP_URL: ${{ secrets.APP_URL }}
CRON_SECRET: ${{ secrets.CRON_SECRET }}
運用では、別の定期処理で古いセッションも片付けます。TypeScript API referenceでは client.beta.agents.sessions.delete(sessionID) が案内されています。たとえば30日以上前に完了した台帳を対象にし、OpenAI側のセッション削除とFirestore側の状態更新を分けて記録します。
await openai.beta.agents.sessions.delete(openaiSessionId);
await ledgerRef.update({
status: "completed",
deletedRemoteSessionAt: FieldValue.serverTimestamp(),
});
実務での注意点
Agents APIを導入するときは、「AIに何をさせるか」より先に「アプリとして何を保証するか」を決めるのが大切です。
| 観点 | 最初に決めること |
|---|---|
| 認可 | セッション作成前にプロジェクト閲覧権限を確認する |
| 保存 | Issue本文や顧客情報をイベントログへ丸ごと複製しない |
| 追跡 | Firestoreに openaiSessionId とアプリ側の ledgerId を両方残す |
| 再実行 | 失敗したセッションを人が確認してから再投入できるようにする |
| 削除 | 完了後の保持期間を決め、リモートセッション削除も台帳に残す |
20〜30件のIssueを一括で処理する場合は、全件を同時に投げず、3〜5件ずつバッチにするのが扱いやすいです。AIの実行時間だけでなく、Firestore書き込み、GitHub API、レビュー担当者の確認負荷も同時に増えるからです。
まとめ
Agents APIのpublic betaは、AIエージェントを「単発のAPI呼び出し」から「業務アプリ内の長時間セッション」へ引き上げるための更新です。
- 2026年9月10日にOpenAI Agents APIがpublic betaとして公開された
- 新規の管理されたエージェント実行には、公式ドキュメント上でAgents APIが開始点として案内されている
- Next.js側では、作成前の認可、Firestore台帳、イベント同期、削除ポリシーを持つ
- GitHub Actionsの定期実行で、進行中セッションのイベントを少しずつ同期する
- 本文やイベントpayloadを保存しすぎず、監査に必要な最小情報から始める
AIエージェントをチームの開発フローに入れるなら、賢いプロンプトだけでなく、セッションの持ち方が効いてきます。まずはIssue調査やリリースノート下書きのように、失敗しても人が確認しやすい領域から台帳付きで始めるのがおすすめです。