create-agent-adapter について
PaperclipのAgentアダプターを開発し、各種エージェントランタイムに接続するためのガイドです。サーバー、UI、CLIの各利用側に必要なモジュール、診断、登録手順を説明します。
主な機能
- 01サーバー・UI・CLIモジュールの定義
- 02実行とセッション処理の仕様
- 03出力の解析と実行記録への変換
- 04設定フォームと環境診断の構築
- 05アダプター登録チェックリスト
活用シーン
- 01新しいエージェントランタイム用アダプターの作成
- 02CLIまたはHTTPによる実行処理の追加
- 03アダプター設定と事前診断の実装
- 04Paperclipの各利用側へのアダプター登録
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ paperclipai/ paperclip --skill create-agent-adapterインストール後、Claude Code で/create-agent-adapterを入力して読み込まれたか確認します。
詳細
Paperclip エージェントアダプターの作成
アダプターは、Paperclip のオーケストレーション層と特定の AI エージェントランタイム(Claude Code、Codex CLI、カスタムプロセス、HTTP エンドポイントなど)をつなぎます。各アダプターは自己完結型のパッケージで、サーバー、UI、CLI の3 つのコンシューマー向けに実装を提供します。
1. アーキテクチャの概要
packages/adapters/<name>/
src/
index.ts # Shared metadata (type, label, models, agentConfigurationDoc)
server/
index.ts # Server exports: execute, sessionCodec, parse helpers
execute.ts # Core execution logic (AdapterExecutionContext -> AdapterExecutionResult)
parse.ts # Stdout/result parsing for the agent's output format
ui/
index.ts # UI exports: parseStdoutLine, buildConfig
parse-stdout.ts # Line-by-line stdout -> TranscriptEntry[] for the run viewer
build-config.ts # CreateConfigValues -> adapterConfig JSON for agent creation form
cli/
index.ts # CLI exports: formatStdoutEvent
format-event.ts # Colored terminal output for `paperclipai run --watch`
package.json
tsconfig.json
3 つの個別のレジストリがアダプターモジュールを使用します。
| レジストリ | 場所 | インターフェース |
|---|---|---|
| サーバー | server/src/adapters/registry.ts | ServerAdapterModule |
| UI | ui/src/adapters/registry.ts | UIAdapterModule |
| CLI | cli/src/adapters/registry.ts | CLIAdapterModule |
2. 共有型(@paperclipai/adapter-utils)
すべてのアダプターインターフェースは packages/adapter-utils/src/types.ts にあります。@paperclipai/adapter-utils(型)または @paperclipai/adapter-utils/server-utils(ランタイムヘルパー)からインポートしてください。
コアインターフェース
// The execute function signature — every adapter must implement this
interface AdapterExecutionContext {
runId: string;
agent: AdapterAgent; // { id, companyId, name, adapterType, adapterConfig }
runtime: AdapterRuntime; // { sessionId, sessionParams, sessionDisplayId, taskKey }
config: Record<string, unknown>; // The agent's adapterConfig blob
context: Record<string, unknown>; // Runtime context (taskId, wakeReason, approvalId, etc.)
onLog: (stream: "stdout" | "stderr", chunk: string) => Promise<void>;
onMeta?: (meta: AdapterInvocationMeta) => Promise<void>;
authToken?: string;
}
interface AdapterExecutionResult {
exitCode: number | null;
signal: string | null;
timedOut: boolean;
errorMessage?: string | null;
usage?: UsageSummary; // { inputTokens, outputTokens, cachedInputTokens? }
sessionId?: string | null; // Legacy — prefer sessionParams
sessionParams?: Record<string, unknown> | null; // Opaque session state persisted between runs
sessionDisplayId?: string | null;
provider?: string | null; // "anthropic", "openai", etc.
model?: string | null;
costUsd?: number | null;
resultJson?: Record<string, unknown> | null;
summary?: string | null; // Human-readable summary of what the agent did
clearSession?: boolean; // true = tell Paperclip to forget the stored session
}
interface AdapterSessionCodec {
deserialize(raw: unknown): Record<string, unknown> | null;
serialize(params: Record<string, unknown> | null): Record<string, unknown> | null;
getDisplayId?(params: Record<string, unknown> | null): string | null;
}
モジュールインターフェース
// Server — registered in server/src/adapters/registry.ts
interface ServerAdapterModule {
type: string;
execute(ctx: AdapterExecutionContext): Promise<AdapterExecutionResult>;
testEnvironment(ctx: AdapterEnvironmentTestContext): Promise<AdapterEnvironmentTestResult>;
sessionCodec?: AdapterSessionCodec;
supportsLocalAgentJwt?: boolean;
models?: { id: string; label: string }[];
agentConfigurationDoc?: string;
}
// UI — registered in ui/src/adapters/registry.ts
interface UIAdapterModule {
type: string;
label: string;
parseStdoutLine: (line: string, ts: string) => TranscriptEntry[];
ConfigFields: ComponentType<AdapterConfigFieldsProps>;
buildAdapterConfig: (values: CreateConfigValues) => Record<string, unknown>;
}
// CLI — registered in cli/src/adapters/registry.ts
interface CLIAdapterModule {
type: string;
formatStdoutEvent: (line: string, debug: boolean) => void;
}
2.1 アダプター環境テストの契約
すべてのサーバーアダプターは testEnvironment(...) を実装する必要があります。これにより、エージェント設定画面のボード UI にある「環境をテスト」ボタンが機能します。
type AdapterEnvironmentCheckLevel = "info" | "warn" | "error";
type AdapterEnvironmentTestStatus = "pass" | "warn" | "fail";
interface AdapterEnvironmentCheck {
code: string;
level: AdapterEnvironmentCheckLevel;
message: string;
detail?: string | null;
hint?: string | null;
}
interface AdapterEnvironmentTestResult {
adapterType: string;
status: AdapterEnvironmentTestStatus;
checks: AdapterEnvironmentCheck[];
testedAt: string; // ISO timestamp
}
interface AdapterEnvironmentTestContext {
companyId: string;
adapterType: string;
config: Record<string, unknown>; // runtime-resolved adapterConfig
}
ガイドライン:
- 構造化された診断情報を返し、想定される検出結果について例外をスローしないでください。
- 無効または使用不能なランタイム設定(不正な cwd、コマンドの欠落、無効な URL)には
errorを使用します。 - 処理を妨げないものの重要な状況には
warnを使用します。 - 成功したチェックやコンテキストには
infoを使用します。
重大度ポリシーは、製品にとって極めて重要です。警告は保存を妨げません。
例:claude_local では、明示的に設定された ANTHROPIC_API_KEY または選択されたマネージド API 接続は info です。これはユーザーが選択した認証方法だからです。サブスクリプションログインを上書きする環境由来のサーバーキーは、error ではなく warn のままにします。
3. 手順:新しいアダプターの作成
3.1 パッケージの作成
packages/adapters/<name>/
package.json
tsconfig.json
src/
index.ts
server/index.ts
server/execute.ts
server/parse.ts
ui/index.ts
ui/parse-stdout.ts
ui/build-config.ts
cli/index.ts
cli/format-event.ts
package.json — 4 つのエクスポート規約に従う必要があります。
{
"name": "@paperclipai/adapter-<name>",
"version": "0.0.1",
"private": true,
"type": "module",
"exports": {
".": "./src/index.ts",
"./server": "./src/server/index.ts",
"./ui": "./src/ui/index.ts",
"./cli": "./src/cli/index.ts"
},
"dependencies": {
"@paperclipai/adapter-utils": "workspace:*",
"picocolors": "^1.1.1"
},
"devDependencies": {
"typescript": "^5.7.3"
}
}
3.2 ルートの index.ts — アダプターのメタデータ
このファイルは3 つすべてのコンシューマー(サーバー、UI、CLI)によってインポートされます。依存関係を持たせないでください(Node API や React は使用しないでください)。
export const type = "my_agent"; // snake_case, globally unique
export const label = "My Agent (local)";
export const models = [
{ id: "model-a", label: "Model A" },
{ id: "model-b", label: "Model B" },
];
export const agentConfigurationDoc = `# my_agent agent configuration
...document all config fields here...
`;
必須のエクスポート:
type— アダプターの型キー。agents.adapter_typeに保存されますlabel— UI に表示する、人が読んで分かりやすい名前models— エージェント作成フォームで選択できるモデルagentConfigurationDoc— すべてのadapterConfigフィールドを説明する Markdown(他のエージェントを設定する LLM エージェントが使用)
ルーティングロジックとしての agentConfigurationDoc の作成:
agentConfigurationDoc は、他のエージェントを作成する Paperclip エージェントを含む LLM エージェントによって参照されます。宣伝文句ではなく、ルーティングロジックとして記述してください。LLM が特定のタスクにこのアダプターが適切かどうか判断できるよう、具体的な「使用する場合」と「使用しない場合」の指針を含めてください。
export const agentConfigurationDoc = `# my_agent agent configuration
Adapter: my_agent
Use when:
- The agent needs to run MyAgent CLI locally on the host machine
- You need session persistence across runs (MyAgent supports thread resumption)
- The task requires MyAgent-specific tools (e.g. web search, code execution)
Don't use when:
- You need a simple one-shot script execution (use the "process" adapter instead)
- The agent doesn't need conversational context between runs (process adapter is simpler)
- MyAgent CLI is not installed on the host
Core fields:
- cwd (string, required): absolute working directory for the agent process
...
`;
明確な否定例を加えると、アダプターの選択精度が向上します。具体的なアンチパターンを 1 つ示すほうが、説明を 3 段落加えるより効果的です。
3.3 サーバーモジュール
server/execute.ts — コア
これは最も重要なファイルです。AdapterExecutionContext を受け取り、AdapterExecutionResult を返す必要があります。
必須の動作:
- 設定を読み取る — ヘルパー(
@paperclipai/adapter-utils/server-utilsのasString、asNumber、asBoolean、asStringArray、parseObject)を使用して、ctx.configから型付きの値を抽出します - 環境を構築する —
buildPaperclipEnv(agent)を呼び出した後、PAPERCLIP_RUN_ID、コンテキスト変数(PAPERCLIP_TASK_ID、PAPERCLIP_WAKE_REASON、PAPERCLIP_WAKE_COMMENT_ID、PAPERCLIP_APPROVAL_ID、PAPERCLIP_APPROVAL_STATUS、PAPERCLIP_LINKED_ISSUE_IDS)、ユーザーの環境変数による上書き、認証トークンを追加します - セッションを解決する — 既存のセッションがあるか、
runtime.sessionParams/runtime.sessionIdを確認します。互換性(例:同じ cwd であること)を検証し、再開するか新しく開始するかを決定します - プロンプトをレンダリングする — テンプレート変数
agentId、companyId、runId、company、agent、run、contextを指定してrenderTemplate(template, data)を使用します - onMeta を呼び出す — プロセスを起動する前に、アダプター呼び出しのメタデータを出力します
- プロセスを起動する — CLI ベースのエージェントには
runChildProcess()を、HTTP ベースのエージェントにはfetch()を使用します - 出力を解析する — エージェントの標準出力を構造化データ(セッション ID、使用量、要約、エラー)に変換します
- セッションエラーを処理する — 再開時に「unknown session」エラーが発生した場合は、新しいセッションで再試行し、
clearSession: trueを設定します - AdapterExecutionResult を返す — エージェントのランタイムがサポートするすべてのフィールドを設定します
サーバーが常に注入する環境変数:
| 変数 | ソース |
|---|---|
PAPERCLIP_AGENT_ID | agent.id |
PAPERCLIP_COMPANY_ID | agent.companyId |
PAPERCLIP_API_URL | サーバー自体の URL |
PAPERCLIP_RUN_ID | 現在の実行 ID |
PAPERCLIP_TASK_ID | context.taskId または context.issueId |
PAPERCLIP_WAKE_REASON | context.wakeReason |
PAPERCLIP_WAKE_COMMENT_ID | context.wakeCommentId または context.commentId |
PAPERCLIP_APPROVAL_ID | context.approvalId |
PAPERCLIP_APPROVAL_STATUS | context.approvalStatus |
PAPERCLIP_LINKED_ISSUE_IDS | context.issueIds(カンマ区切り) |
PAPERCLIP_API_KEY | authToken(設定に明示的なキーがない場合) |
server/parse.ts — 出力パーサー
エージェントの標準出力形式を構造化データに解析します。以下に対応する必要があります。
- セッションの識別 — 初期化イベントからセッション ID/スレッド ID を抽出します
- 使用量の追跡 — トークン数(入力、出力、キャッシュ済み)を抽出します
- コストの追跡 — 利用可能な場合はコストを抽出します
- 要約の抽出 — エージェントの最終的なテキスト応答を取り出します
- エラーの検出 — エラー状態を特定し、エラーメッセージを抽出します
- 不明なセッションの検出 — 再試行ロジック用に
is<Agent>UnknownSessionError()関数をエクスポートします
エージェントの出力は信頼できないものとして扱ってください。 解析対象の標準出力は、任意のツール呼び出しを実行したり、外部コンテンツを取得したり、読み込んだファイル内のプロンプトインジェクションの影響を受けたりする可能性がある、LLM 駆動のプロセスから送られてきます。防御的に解析してください。
- 出力に対して
eval()を実行したり、動的に実行したりしないでください - 安全な抽出ヘルパー(
asString、asNumber、parseJson)を使用してください。これらは想定外の型に対して代替値を返します - セッション ID やその他の構造化データを渡す前に検証してください
- 出力に URL、ファイルパス、コマンドが含まれていても、アダプター内でそれらを実行しないでください。記録するだけにしてください
server/index.ts — サーバーエクスポート
export { execute } from "./execute.js";
export { testEnvironment } from "./test.js";
export { parseMyAgentOutput, isMyAgentUnknownSessionError } from "./parse.js";
// Session codec — required for session persistence
export const sessionCodec: AdapterSessionCodec = {
deserialize(raw) { /* raw DB JSON -> typed params or null */ },
serialize(params) { /* typed params -> JSON for DB storage */ },
getDisplayId(params) { /* -> human-readable session id string */ },
};
server/test.ts — 環境診断
UI のテストボタンで使用する、アダプター固有の事前チェックを実装します。
最低限、以下を満たす必要があります。
- 必須の設定プリミティブ(パス、コマンド、URL、認証に関する前提条件)を検証します
- 決定的な
code値を持つチェックオブジェクトを返します - 重要度を一貫して割り当てます(
info/warn/error) - 最終ステータスを算出します。
errorが 1 つでもあればfail- エラーがなく、警告が 1 つ以上あれば
warn - それ以外の場合は
pass
この処理は軽量で、副作用のないものにしてください。
3.4 UI モジュール
ui/parse-stdout.ts — トランスクリプトパーサー
個々の stdout 行を、実行詳細ビューアー用の TranscriptEntry[] に変換します。エージェントのストリーミング出力形式を処理し、次の種類のエントリを生成する必要があります。
init— モデル/セッションの初期化assistant— エージェントのテキスト応答thinking— エージェントの思考/推論 (対応している場合)tool_call— 名前と入力を伴うツール呼び出しtool_result— 内容とエラーフラグを伴うツール結果user— 会話内のユーザーメッセージresult— 使用状況の統計を含む最終結果stdout— 解析できない行のフォールバック
export function parseMyAgentStdoutLine(line: string, ts: string): TranscriptEntry[] {
// Parse JSON line, map to appropriate TranscriptEntry kind(s)
// Return [{ kind: "stdout", ts, text: line }] as fallback
}
ui/build-config.ts — 設定ビルダー
UI フォームの CreateConfigValues を、エージェントに保存される adapterConfig JSON blob に変換します。
export function buildMyAgentConfig(v: CreateConfigValues): Record<string, unknown> {
const ac: Record<string, unknown> = {};
if (v.cwd) ac.cwd = v.cwd;
if (v.promptTemplate) ac.promptTemplate = v.promptTemplate;
if (v.model) ac.model = v.model;
ac.timeoutSec = 0;
ac.graceSec = 15;
// ... adapter-specific fields
return ac;
}
UI 設定フィールドコンポーネント
AdapterConfigFieldsProps を実装する React コンポーネントを ui/src/adapters/<name>/config-fields.tsx に作成します。これはエージェントの作成/編集フォームにアダプター固有のフォームフィールドをレンダリングします。
ui/src/components/agent-config-primitives の共有プリミティブを使用します。
Field— ラベル付きフォームフィールドのラッパーToggleField— ラベルとヒントを伴うブール値トグルDraftInput— ドラフト/コミット動作を伴うテキスト入力DraftNumberInput— ドラフト/コミット動作を伴う数値入力help— 一般的なフィールド向けの標準ヒントテキスト
コンポーネントは create モード (values/set を使用) と edit モード (config/eff/mark を使用) の両方をサポートする必要があります。
3.5 CLI モジュール
cli/format-event.ts — ターミナルフォーマッター
paperclipai run --watch 用に stdout 行を整形して表示します。色付けには picocolors を使用します。
import pc from "picocolors";
export function printMyAgentStreamEvent(raw: string, debug: boolean): void {
// Parse JSON line from agent stdout
// Print colored output: blue for system, green for assistant, yellow for tools
// In debug mode, print unrecognized lines in gray
}
4. 登録チェックリスト
アダプターパッケージを作成したら、3 つのコンシューマーすべてに登録します。
4.1 サーバーレジストリ (server/src/adapters/registry.ts)
import { execute as myExecute, sessionCodec as mySessionCodec } from "@paperclipai/adapter-my-agent/server";
import { agentConfigurationDoc as myDoc, models as myModels } from "@paperclipai/adapter-my-agent";
const myAgentAdapter: ServerAdapterModule = {
type: "my_agent",
execute: myExecute,
sessionCodec: mySessionCodec,
models: myModels,
supportsLocalAgentJwt: true, // true if agent can use Paperclip API
agentConfigurationDoc: myDoc,
};
// Add to the adaptersByType map
const adaptersByType = new Map<string, ServerAdapterModule>(
[..., myAgentAdapter].map((a) => [a.type, a]),
);
4.2 UI レジストリ (ui/src/adapters/registry.ts)
import { myAgentUIAdapter } from "./my-agent";
const adaptersByType = new Map<string, UIAdapterModule>(
[..., myAgentUIAdapter].map((a) => [a.type, a]),
);
ui/src/adapters/my-agent/index.ts を使用します。
import type { UIAdapterModule } from "../types";
import { parseMyAgentStdoutLine } from "@paperclipai/adapter-my-agent/ui";
import { MyAgentConfigFields } from "./config-fields";
import { buildMyAgentConfig } from "@paperclipai/adapter-my-agent/ui";
export const myAgentUIAdapter: UIAdapterModule = {
type: "my_agent",
label: "My Agent",
parseStdoutLine: parseMyAgentStdoutLine,
ConfigFields: MyAgentConfigFields,
buildAdapterConfig: buildMyAgentConfig,
};
4.3 CLI レジストリ (cli/src/adapters/registry.ts)
import { printMyAgentStreamEvent } from "@paperclipai/adapter-my-agent/cli";
const myAgentCLIAdapter: CLIAdapterModule = {
type: "my_agent",
formatStdoutEvent: printMyAgentStreamEvent,
};
// Add to the adaptersByType map
5. セッション管理 — 長時間実行のための設計
セッションにより、エージェントは実行をまたいで会話コンテキストを維持できます。このシステムは コーデックベース です — 各アダプターがセッション状態をシリアライズ/デシリアライズする方法を定義します。
最初から長時間実行を想定して設計してください。 セッションの再利用は、後から追加する最適化ではなく、デフォルトのプリミティブとして扱ってください。Issue に取り組むエージェントは、何十回も起こされる可能性があります — 最初の割り当て、承認コールバック、再割り当て、手動のナッジなどです。各起動は既存の会話を再開し、エージェントがすでに何を行い、どのファイルを読み、どのような決定を下したかについての完全なコンテキストを保持できるようにする必要があります。毎回新規に開始すると、同じファイルを再読み込みするためにトークンを浪費し、矛盾した決定を下すリスクがあります。
主要な概念:
sessionParamsは、タスクごとに DB に保存される不透明なRecord<string, unknown>です- アダプターの
sessionCodec.serialize()は、実行結果データを保存可能なパラメーターに変換します sessionCodec.deserialize()は、保存されたパラメーターを次の実行のために元に戻しますsessionCodec.getDisplayId()は、UI 用に人間が読めるセッション ID を抽出します- cwd を考慮した再開: セッションが現在の設定とは異なる cwd で作成された場合、再開をスキップします (プロジェクトをまたぐセッションの混入を防ぎます)
- 不明なセッションの再試行: 再開が「セッションが見つかりません」エラーで失敗した場合、新しいセッションで再試行し、
clearSession: trueを返して Paperclip が古いセッションを消去できるようにします
エージェントランタイムが何らかの形式のコンテキスト圧縮や会話圧縮 (たとえば Claude Code の自動コンテキスト管理や Codex の previous_response_id チェーン) をサポートしている場合は、それを活用してください。セッション再開をサポートするアダプターは圧縮を無料で得られます — エージェントランタイムが再開をまたいでコンテキストウィンドウ管理を内部的に処理します。
パターン (claude-local と codex-local の両方から):
const canResumeSession =
runtimeSessionId.length > 0 &&
(runtimeSessionCwd.length === 0 || path.resolve(runtimeSessionCwd) === path.resolve(cwd));
const sessionId = canResumeSession ? runtimeSessionId : null;
// ... run attempt ...
// If resume failed with unknown session, retry fresh
if (sessionId && !proc.timedOut && exitCode !== 0 && isUnknownSessionError(output)) {
const retry = await runAttempt(null);
return toResult(retry, { clearSessionOnMissingSession: true });
}
6. Server-Utils ヘルパー
@paperclipai/adapter-utils/server-utils からインポートします。
| ヘルパー | 目的 |
|---|---|
asString(val, fallback) | 文字列を安全に抽出します |
asNumber(val, fallback) | 数値を安全に抽出します |
asBoolean(val, fallback) | 真偽値を安全に抽出します |
asStringArray(val) | 文字列配列を安全に抽出します |
parseObject(val) | Record<string, unknown> を安全に抽出します |
parseJson(str) | Record または null を返す安全な JSON.parse です |
renderTemplate(tmpl, data) | {{path.to.value}} 形式のテンプレートをレンダリングします |
buildPaperclipEnv(agent) | 標準の PAPERCLIP_* 環境変数を構築します |
redactEnvForLogs(env) | onMeta 用に機密性の高いキーを伏せます |
ensureAbsoluteDirectory(cwd) | cwd が存在し、絶対パスであることを検証します |
ensureCommandResolvable(cmd, cwd, env) | コマンドが PATH に存在することを検証します |
ensurePathInEnv(env) | env に PATH が存在することを確認します |
runChildProcess(runId, cmd, args, opts) | タイムアウト、ログ記録、出力のキャプチャに対応してプロセスを起動します |
7. 規約とパターン
命名
- アダプターの種類:
snake_case(例:claude_local、codex_local) - パッケージ名:
@paperclipai/adapter-<kebab-name> - パッケージディレクトリ:
packages/adapters/<kebab-name>/
設定の解析
configの値をそのまま信頼しないでください。必ずasString、asNumberなどを使用します。- すべての省略可能なフィールドに妥当なデフォルト値を設定します。
- すべてのフィールドを
agentConfigurationDocに記載します。
プロンプトテンプレート
- すべての実行で
promptTemplateをサポートします。 - 標準の変数セットを使用して
renderTemplate()を呼び出します。 - デフォルトのプロンプトには、
@paperclipai/adapter-utils/server-utilsのDEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATEを使用します。これにより、ローカルアダプター間で Paperclip の実行契約を共有できます。同じハートビート内で動作し、要求がない限り計画だけで終了せず、永続的な進捗と次のアクションを残し、ポーリングの代わりに子課題を使用し、ブロッカーには担当者とアクションを明記し、ガバナンスの境界を守ります。
エラー処理
- タイムアウト、プロセスエラー、解析失敗を区別します。
- 失敗時は必ず
errorMessageを設定します。 - 解析に失敗した場合は、生の stdout/stderr を
resultJsonに含めます。 - エージェントの CLI がインストールされていない場合(コマンドが見つからない場合)に対処します。
ログ記録
- すべてのプロセス出力に対して
onLog("stdout", ...)とonLog("stderr", ...)を呼び出します。これにより、リアルタイムの実行ビューアーに情報が送られます。 - 起動前に
onMeta(...)を呼び出し、呼び出しの詳細を記録します。 - メタに env を含める場合は
redactEnvForLogs()を使用します。
Paperclip スキルの注入
Paperclip には、実行時にエージェントが必要とする共有スキル(リポジトリのトップレベルにある skills/ ディレクトリ内)が含まれています。たとえば、paperclip API スキルや paperclip-create-agent ワークフロースキルなどです。各アダプターは、エージェントの作業ディレクトリを汚染することなく、エージェントの実行環境からこれらのスキルを検出できるようにする責任があります。
制約: スキルをエージェントの cwd にコピーしたり、シンボリックリンクを作成したりしないでください。cwd はユーザーのプロジェクトのチェックアウト先です。.claude/skills/ やその他のファイルを書き込むと、リポジトリが Paperclip の内部ファイルで汚染され、git のステータスに影響し、コミットに混入するおそれがあります。
方法: スキル用にクリーンで分離された場所を作成し、そこを参照するようエージェントの実行環境に指示します。
claude-local の方法:
- 実行時に新しい一時ディレクトリを作成します:
mkdtemp("paperclip-skills-") - その中に
.claude/skills/を作成します(Claude Code が想定するディレクトリ構成です) - リポジトリの
skills/にある各スキルディレクトリへのシンボリックリンクを、一時ディレクトリの.claude/skills/内に作成します --add-dir <tmpdir>で一時ディレクトリを Claude Code に渡します。これにより、エージェントの実際の cwd に触れることなく、Claude Code はそのディレクトリに登録されたかのようにスキルを検出できます- 実行完了後、
finallyブロックで一時ディレクトリをクリーンアップします
// From claude-local execute.ts
async function buildSkillsDir(): Promise<string> {
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-skills-"));
const target = path.join(tmp, ".claude", "skills");
await fs.mkdir(target, { recursive: true });
const entries = await fs.readdir(PAPERCLIP_SKILLS_DIR, { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory()) {
await fs.symlink(
path.join(PAPERCLIP_SKILLS_DIR, entry.name),
path.join(target, entry.name),
);
}
}
return tmp;
}
// In execute(): pass --add-dir to Claude Code
const skillsDir = await buildSkillsDir();
args.push("--add-dir", skillsDir);
// ... run process ...
// In finally: fs.rm(skillsDir, { recursive: true, force: true })
codex-local の方法:
Codex には、グローバルな個人用スキルディレクトリ($CODEX_HOME/skills または ~/.codex/skills)があります。アダプターは、まだ存在しない場合に Paperclip のスキルへのシンボリックリンクをそこに作成します。これはユーザーのプロジェクトではなく、エージェントツール自体の設定ディレクトリなので問題ありません。
// From codex-local execute.ts
async function ensureCodexSkillsInjected(onLog) {
const skillsHome = path.join(codexHomeDir(), "skills");
await fs.mkdir(skillsHome, { recursive: true });
for (const entry of entries) {
const target = path.join(skillsHome, entry.name);
const existing = await fs.lstat(target).catch(() => null);
if (existing) continue; // Don't overwrite user's own skills
await fs.symlink(source, target);
}
}
新しいアダプターを作成する場合: エージェントの実行環境がスキルやプラグインをどのように検出するかを調べ、最もクリーンな注入方法を選択してください。
- 最適:一時ディレクトリ+フラグ(claude-local のような方法) — 実行環境が「追加ディレクトリ」フラグに対応している場合は、一時ディレクトリを作成し、そこにスキルへのシンボリックリンクを作成してフラグで渡し、処理後にクリーンアップします。副作用は一切ありません。
- 許容可能:グローバル設定ディレクトリ(codex-local のような方法) — 実行環境にプロジェクトとは別のグローバルなスキル/プラグインディレクトリがある場合は、そこにシンボリックリンクを作成します。ユーザーのカスタマイズを上書きしないよう、既存の項目はスキップしてください。
- 許容可能:環境変数 — 実行環境が環境変数からスキル/プラグインのパスを読み取る場合は、リポジトリの
skills/ディレクトリを直接指定します。 - 最終手段:プロンプトへの埋め込み — 実行環境にプラグインシステムがない場合は、スキルの内容をプロンプトテンプレート自体に含めます。トークンは消費しますが、ファイルシステムへの副作用は完全に避けられます。
スキルはプロンプトを肥大化させるものではなく、読み込んで使う手順です。 Paperclip のスキル(paperclip や paperclip-create-agent など)は、必要に応じて使う手順として設計されています。エージェントはコンテキスト内でスキルのメタデータ(名前と説明)を確認し、スキルの実行を決めたときにのみ、SKILL.md の内容全体を読み込みます。これにより、基本プロンプトを小さく保てます。アダプターの agentConfigurationDoc やプロンプトテンプレートを作成するときは、スキルの内容を直接埋め込まないでください。エージェントの実行環境によるスキル検出に任せてください。各 SKILL.md のフロントマターにある説明は、ルーティングロジックとして機能します。スキルの内容ではなく、完全なスキルを読み込むタイミングをエージェントに伝えます。
明示的なスキル呼び出しと曖昧なスキル呼び出し。 信頼性が重要な本番ワークフロー(たとえば、ステータス報告のために必ず Paperclip API を呼び出す必要があるエージェント)では、プロンプトテンプレートに明示的な指示を記述してください。「進捗を報告するには paperclip スキルを使用してください。」と指示します。モデルに説明との照合をもとに判断させる曖昧なルーティングは、探索的なタスクには適していますが、必須手順には信頼性がありません。
8. セキュリティに関する考慮事項
アダプターは、Paperclip のオーケストレーション層と任意のエージェント実行との境界に位置します。これはリスクの高い領域です。
エージェントの出力を信頼できないものとして扱う
エージェントプロセスは、外部ファイルを読み取り、URL を取得し、ツールを実行する LLM 駆動のコードを実行します。その出力は、処理対象のコンテンツに含まれるプロンプトインジェクションの影響を受ける可能性があります。アダプターの解析レイヤーは信頼境界です。すべてを検証し、何も実行しないでください。
プロンプトではなく環境変数によるシークレットの注入
シークレット(API キー、トークン)をプロンプトテンプレートや LLM を経由する設定フィールドに絶対に入れないでください。代わりに、エージェントのツールが直接読み取れる環境変数として注入します。
PAPERCLIP_API_KEYはプロンプトではなく、サーバーによってプロセス環境に注入されますconfig.env内のユーザー提供のシークレットは環境変数として渡され、onMetaログではマスクされますredactEnvForLogs()ヘルパーは、/(key|token|secret|password|authorization|cookie)/iに一致するあらゆるキーを自動的にマスクします
これは「サイドカー注入」パターンに従っています。モデルは実際のシークレット値を決して目にしませんが、呼び出すツールは環境からそれを読み取ることができます。
ネットワークアクセス
エージェントランタイムがネットワークアクセス制御(サンドボックス化、許可リスト)をサポートしている場合は、アダプターでそれらを設定してください。
- オープンなインターネットアクセスよりも最小限の許可リストを優先してください。Paperclip API と GitHub を呼び出すだけでよいエージェントは、任意のホストにアクセスできるべきではありません。
- スキル + ネットワーク = 増幅されたリスク。エージェントに HTTP リクエストを送信させるスキルと、無制限のネットワークアクセスが組み合わさると、データ持ち出しの経路が生まれます。どちらか一方を制限してください。
- ランタイムが階層化ポリシー(組織レベルのデフォルト + リクエストごとのオーバーライド)をサポートしている場合は、組織レベルのポリシーをアダプター設定に組み込み、エージェントごとの設定でさらに絞り込めるようにしてください。
プロセスの分離
- CLI ベースのアダプターは、サーバーのユーザー権限を継承します。
cwdとenvの設定によって、エージェントプロセスがファイルシステム上で何にアクセスできるかが決まります。 dangerouslySkipPermissions/dangerouslyBypassApprovalsAndSandboxフラグは開発の便宜のために存在しますが、agentConfigurationDocで危険であると明記する必要があります。本番環境のデプロイではこれらを使用しないでください。- タイムアウトと猶予期間(
timeoutSec、graceSec)は安全レールです。常にこれらを適用してください。タイムアウトのない暴走したエージェントプロセスは、無制限のリソースを消費する可能性があります。
9. TranscriptEntry の種類リファレンス
UI の実行ビューアは、これらのエントリの種類を表示します。
| 種類 | フィールド | 用途 |
|---|---|---|
init | model、sessionId | エージェントの初期化 |
assistant | text | エージェントのテキスト応答 |
thinking | text | エージェントの推論/思考 |
user | text | ユーザーメッセージ |
tool_call | name、input | ツールの呼び出し |
tool_result | toolUseId、content、isError | ツールの結果 |
result | text、inputTokens、outputTokens、cachedTokens、costUsd、subtype、isError、errors | 使用量を含む最終結果 |
stderr | text | 標準エラー出力 |
system | text | システムメッセージ |
stdout | text | 生の標準出力へのフォールバック |
10. テスト
server/src/__tests__/<adapter-name>-adapter.test.ts にテストを作成してください。テストする内容:
- 出力のパース — サンプルの標準出力をパーサーに通し、構造化された出力を検証する
- 不明なセッションの検出 —
is<Agent>UnknownSessionError関数を検証する - 設定の構築 —
buildConfigがフォームの値から正しい adapterConfig を生成することを検証する - セッションコーデック — シリアライズ/デシリアライズのラウンドトリップを検証する
11. 最小限のアダプターのチェックリスト
-
packages/adapters/<name>/package.jsonに 4 つのエクスポート(.,./server,./ui,./cli)を定義する - ルートの
index.tsにtype、label、models、agentConfigurationDocを定義する -
server/execute.tsでAdapterExecutionContext -> AdapterExecutionResultを実装する -
server/test.tsでAdapterEnvironmentTestContext -> AdapterEnvironmentTestResultを実装する -
server/parse.tsに出力パーサーと不明なセッションの検出機能を実装する -
server/index.tsでexecute、testEnvironment、sessionCodec、パース用ヘルパーをエクスポートする -
ui/parse-stdout.tsに実行ビューアー用のStdoutLineParserを実装する -
ui/build-config.tsにCreateConfigValues -> adapterConfigビルダーを実装する -
ui/src/adapters/<name>/config-fields.tsxにエージェントフォーム用の React コンポーネントを実装する -
ui/src/adapters/<name>/index.tsでUIAdapterModuleを組み立てる -
cli/format-event.tsにターミナル用フォーマッターを実装する -
cli/index.tsでフォーマッターをエクスポートする -
server/src/adapters/registry.tsに登録する -
ui/src/adapters/registry.tsに登録する -
cli/src/adapters/registry.tsに登録する - ルートの
pnpm-workspace.yamlのワークスペースに追加する(グロブでカバーされていない場合) - パース、セッションコーデック、設定のビルドのテストを追加する