SNA

はじめに

SNA の構造を理解し、サーバを起動して最初のセッションにプロンプトを送ります。

空のプロジェクトからライブイベントを受け取るまでの最短経路です。 このページでは、アプリ内で SNA サーバを起動し、Claude Code または Codex / OpenCode のセッションを開き、WebSocket でトークンを ストリーミングします。

SNA でエージェンティックアプリケーションを作る

SNA は エージェンティックアプリケーション を作るためのランタイム層です。 ここでいうエージェントは、単発の回答を返すモデル呼び出しではありません。 プロダクト内でセッション履歴を持ち、ツールを使い、権限を要求し、 状態を UI にストリーミングする実行主体です。

SNA で作れるものは、次のようなものです。

  • セッションを持つプロダクトエージェント。 1 回のプロンプトに答えるだけでなく、 プロジェクト文脈を保ち、複数ターンにわたって続くエージェント。
  • キャッシュに乗りやすい長い会話。 毎ターン履歴やキャッシュ設定を 組み直すのではなく、同じ SNA セッションへメッセージを送り続ける構造。
  • ツールを使うワークフロー。 各 CLI ランタイムがすでに持っている ツール利用の能力をプロダクト機能の中へ取り込む流れ。
  • 人間の承認を挟むアプリケーション。 権限リクエストと承認を プロダクト UI の中で扱うエージェント体験。
  • Runtime harness ベースのエージェント。 低レベルのモデル API の上に ループを作り直さず、Claude Code、Codex、OpenCode がモデルに合わせて 調整しているオーケストレーション層を使うエージェント。
  • マルチランタイム製品。 プロダクト側 API は安定させたまま、下層の エージェントランタイムを切り替えられる構造。

何を作るのか

SNA はアプリに組み込む小さなランタイムサーバです。セッション開始を 要求すると、サーバがエージェント CLI を子プロセスとして起動し、その プロセスを 1 つの HTTP + WebSocket API として公開します。アプリからは 通常のバックエンドにリクエストしているように見えますが、実際の作業は Claude Code / Codex / OpenCode プロセスが処理します。

your app

   │  HTTP for control (send, interrupt, kill, …)
   │  WS   for events (assistant_delta, tool_use, complete, …)

SNA server  ──spawn──►  claude | codex | opencode

以下の 5 ステップに沿って進めてください。正規化履歴、ランタイム切り替え、 アトリビューションなどの設計背景は SNA とは? で扱います。

前提

  • Node.js 20+
  • 少なくとも 1 つのランタイム CLI: PATH 上の claude, codex, opencode のいずれか、または SNA 起動時に渡す明示的な runtimePaths
  • パッケージマネージャ。例では pnpm を使いますが、npm / yarn / bun でも構いません。

1. インストール

pnpm add @sna-sdk/core@0.17.2 @sna-sdk/client@0.17.2

SNA はまだ 0.x.x 系です。アプリで使う場合は、正確なパッケージ バージョンを固定してください。詳しくは インストール にまとめています。 バグや要望は GitHub Issues に残してください。

前提一式と CLI パス上書きの詳細は インストール にまとめています。

2. サーバ起動

アプリ内で:

import { resolveClaudeCli, startSnaServer } from "@sna-sdk/core/node";

const claude = resolveClaudeCli();

const sna = await startSnaServer({
  appId: "my-app",
  port: 3099,
  dbPath: "./data/sna.db",
  runtimePaths: {
    claudeCode: claude.path,
  },
});

SNA サーバは管理された子プロセスとして fork されます。sna.port には 実際のバインドポートが入ります。SDK client には sna.connection を 渡します。sna.appId はこのサーバが作る session の owner metadata として 付きます。port0 を指定した場合は自動割り当てになり、 sna.stop() を呼ぶと SIGTERM でサーバを終了します。

3. クライアント接続

import { SnaClient } from "@sna-sdk/client";

const client = new SnaClient(sna.connection);
client.connect();

4. セッションを開いてエージェントを起動

const { sessionId } = await client.sessions.create({ label: "scratch" });

await client.agent.start(sessionId, {
  provider: "claude-code",
  model: "claude-sonnet-4-6",
});

5. イベント購読 → 送信

client.agent.onEvent(({ event }) => {
  if (event.type === "assistant_delta") process.stdout.write(event.delta as string);
  if (event.type === "tool_use") console.log("\n[tool]", event.message);
  if (event.type === "complete") console.log("\n[done]", event.data);
});
await client.agent.subscribe(sessionId);

await client.agent.send(sessionId, "List the files in this directory.");

これがループ全体です。トークンは assistant_delta で届き、ツール呼び出し は tool_use で届きます。ターンが終わると、使用量とコストを含む complete イベントを受け取れます。

単発呼び出しだけが必要な場合

セッションやイベント購読が不要で、回答 1 つだけが必要な場合は completion() を使います。

import { completion } from "@sna-sdk/core";

const result = await completion({
  prompt: "Summarize: ...",
  provider: "claude-code",
  model: "claude-haiku-4-5",
  onDelta: (chunk) => process.stdout.write(chunk),
});
// result.text, result.usage, result.costUsd, result.durationMs

3 種類のストリーミング(インプロセスコールバック、SSE、フルイベント ストリーム)は Cookbook → ストリーミング で比較しています。

次に

目次