mcpField
C
create-agent-adapter

create-agent-adapter

github.com/paperclipai开发108.3k304 次安装今天更新
代码仓库

关于 create-agent-adapter

面向 Paperclip 扩展开发者的 Agent Adapter 创建指南,说明如何连接不同 Agent 运行时。它涵盖服务器、UI 和 CLI 三个消费者所需的模块、诊断与注册流程。

主要功能

  1. 01定义服务器、UI、CLI 模块
  2. 02规范执行与会话处理
  3. 03解析输出并生成运行记录
  4. 04构建配置表单与环境诊断
  5. 05提供适配器注册检查清单

适用场景

  1. 01为新 Agent 运行时创建适配器
  2. 02添加 CLI 或 HTTP 运行逻辑
  3. 03实现适配器配置和环境预检
  4. 04将适配器接入 Paperclip 消费端

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/paperclipai/paperclip --skill create-agent-adapter

安装后在 Claude Code 中输入/create-agent-adapter确认已加载。

详细介绍

创建 Paperclip Agent 适配器

适配器将 Paperclip 的编排层连接到特定的 AI Agent 运行时(Claude Code、Codex CLI、自定义进程、HTTP 端点等)。每个适配器都是一个独立的软件包,为 三个使用方 提供实现:服务器、UI 和 CLI。


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

三个独立的注册表会使用适配器模块:

注册表位置接口
服务器server/src/adapters/registry.tsServerAdapterModule
UIui/src/adapters/registry.tsUIAdapterModule
CLIcli/src/adapters/registry.tsCLIAdapterModule

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(...)。此功能用于 Agent 配置中看板 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:这是用户选择的身份验证方式。覆盖订阅登录的服务器环境密钥仍属于 warn,而非 error。


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 — 必须采用四导出约定:

{
  "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 — 适配器元数据

此文件会被 所有三个 使用方(服务器、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 — Agent 创建表单中可用的模型选项
  • agentConfigurationDoc — 描述所有 adapterConfig 字段的 Markdown(供配置其他 Agent 的大模型 Agent 使用)

将 agentConfigurationDoc 编写为路由逻辑:

大模型 Agent(包括创建其他 Agent 的 Paperclip Agent)会读取 agentConfigurationDoc。请将其编写为路由逻辑,而非宣传文案。提供具体的“适用于”和“不适用于”指导,让大模型能够判断此适配器是否适合给定任务。

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
...
`;

添加明确的反例可以提高适配器选择的准确性。一个具体的反模式胜过三段描述。

3.3 服务器模块
server/execute.ts — 核心

这是最重要的文件。它接收 AdapterExecutionContext,并且必须返回 AdapterExecutionResult。

必需行为:

  1. 读取配置 — 使用辅助函数(来自 @paperclipai/adapter-utils/server-utils 的 asString、asNumber、asBoolean、asStringArray、parseObject)从 ctx.config 中提取类型化值
  2. 构建环境变量 — 调用 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)、用户环境变量覆盖项和身份验证令牌
  3. 解析会话 — 检查 runtime.sessionParams / runtime.sessionId 中是否存在现有会话;验证其是否兼容(例如,cwd 是否相同);决定是恢复会话还是启动新会话
  4. 渲染提示词 — 使用 renderTemplate(template, data) 和模板变量:agentId、companyId、runId、company、agent、run、context
  5. 调用 onMeta — 在启动进程前发送适配器调用元数据
  6. 启动进程 — 对基于 CLI 的 Agent 使用 runChildProcess(),对基于 HTTP 的 Agent 使用 fetch()
  7. 解析输出 — 将 Agent 的 stdout 转换为结构化数据(会话 ID、用量、摘要、错误)
  8. 处理会话错误 — 如果恢复会话时因“unknown session”失败,则使用新会话重试并设置 clearSession: true
  9. 返回 AdapterExecutionResult — 填充 Agent 运行时支持的所有字段

服务始终注入的环境变量:

变量来源
PAPERCLIP_AGENT_IDagent.id
PAPERCLIP_COMPANY_IDagent.companyId
PAPERCLIP_API_URL服务自身的 URL
PAPERCLIP_RUN_ID当前运行 ID
PAPERCLIP_TASK_IDcontext.taskId 或 context.issueId
PAPERCLIP_WAKE_REASONcontext.wakeReason
PAPERCLIP_WAKE_COMMENT_IDcontext.wakeCommentId 或 context.commentId
PAPERCLIP_APPROVAL_IDcontext.approvalId
PAPERCLIP_APPROVAL_STATUScontext.approvalStatus
PAPERCLIP_LINKED_ISSUE_IDScontext.issueIds(以逗号分隔)
PAPERCLIP_API_KEYauthToken(如果配置中没有显式指定密钥)
server/parse.ts — 输出解析器

将 Agent 的 stdout 格式解析为结构化数据。必须处理:

  • 会话识别 — 从初始化事件中提取会话 / 线程 ID
  • 用量跟踪 — 提取 Token 数量(输入、输出、缓存)
  • 成本跟踪 — 如果可用,提取成本
  • 摘要提取 — 获取 Agent 的最终文本响应
  • 错误检测 — 识别错误状态并提取错误消息
  • 未知会话检测 — 导出 is<Agent>UnknownSessionError() 函数,用于重试逻辑

将 Agent 输出视为不可信内容。 你要解析的 stdout 来自由大模型驱动的进程,该进程可能执行过任意工具调用、获取过外部内容,或受到其读取文件中的提示词注入影响。请进行防御性解析:

  • 切勿对输出使用 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 测试按钮使用。

最低要求:

  1. 验证必需的配置基本项(路径、命令、URL、身份验证假设)
  2. 返回具有确定性 code 值的检查对象
  3. 一致地映射严重级别(info / warn / error)
  4. 计算最终状态:
    • 如果存在任何 error,则为 fail
    • 如果没有错误但至少存在一条警告,则为 warn
    • 否则为 pass

此操作应轻量且无副作用。

3.4 UI 模块
ui/parse-stdout.ts — Transcript Parser

将单独的 stdout 行转换为 TranscriptEntry[],供运行详情查看器使用。必须处理 Agent 的流式输出格式,并生成以下类型的条目:

  • init — 模型/会话初始化
  • assistant — Agent 文本响应
  • thinking — Agent 思考/推理(如果支持)
  • 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 — Config Builder

将 UI 表单的 CreateConfigValues 转换为存储在 Agent 上的 adapterConfig JSON 数据块。

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 Config Fields Component

创建 ui/src/adapters/<name>/config-fields.tsx,其中包含一个实现 AdapterConfigFieldsProps 的 React 组件。它会在 Agent 创建/编辑表单中渲染适配器专属字段。

使用 ui/src/components/agent-config-primitives 中的共享基础组件:

  • Field — 带标签的表单字段包装器
  • ToggleField — 带标签和提示的布尔开关
  • DraftInput — 带草稿/提交行为的文本输入框
  • DraftNumberInput — 带草稿/提交行为的数字输入框
  • help — 常用字段的标准提示文本

该组件必须同时支持 create 模式(使用 values/set)和 edit 模式(使用 config/eff/mark)。

3.5 CLI Module
cli/format-event.ts — Terminal Formatter

为 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. Registration Checklist

创建适配器包后,在以下三个使用方中注册:

4.1 Server Registry (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 Registry (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 Registry (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. Session Management — Designing for Long Runs

会话让 Agent 能够在多次运行之间保留对话上下文。该系统采用基于 codec 的设计——每个适配器都定义了如何序列化/反序列化其会话状态。

从一开始就为长时间运行做好设计。 应将会话复用视为默认基础能力,而不是之后再添加的优化。处理某个问题的 Agent 可能会被唤醒数十次——用于初始分配、审批回调、重新分配和手动提醒。每次唤醒都应恢复已有对话,让 Agent 保留对已完成工作、已读取文件和已做决策的完整上下文。每次都重新开始会浪费 token 来重新读取相同文件,还可能导致决策相互矛盾。

关键概念:

  • sessionParams 是一个不透明的 Record<string, unknown>,按任务存储在数据库中
  • 适配器的 sessionCodec.serialize() 会将执行结果数据转换为可存储的参数
  • sessionCodec.deserialize() 会将已存储的参数转换回数据,供下一次运行使用
  • sessionCodec.getDisplayId() 会提取人类可读的会话 ID,供 UI 使用
  • 感知 cwd 的恢复:如果会话创建时的 cwd 与当前配置中的不同,则跳过恢复(防止不同项目间的会话污染)
  • 未知会话重试:如果恢复因“找不到会话”错误而失败,则使用新会话重试,并返回 clearSession: true,以便 Paperclip 清除过期会话

如果 Agent 运行时支持任何形式的上下文压缩或对话压缩(例如 Claude Code 的自动上下文管理,或 Codex 的 previous_response_id 链接),应尽量利用。支持会话恢复的适配器可免费获得压缩能力——Agent 运行时会在恢复过程中内部处理上下文窗口管理。

模式(来自 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)安全地调用 JSON.parse,返回 Record 或 null
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。
  • 解析失败时,在 resultJson 中包含原始 stdout/stderr。
  • 处理 Agent CLI 未安装的情况(找不到命令)。
日志记录
  • 对所有进程输出调用 onLog("stdout", ...) 和 onLog("stderr", ...),这些输出会提供给实时运行查看器。
  • 在启动进程前调用 onMeta(...),记录调用详情。
  • 在 meta 中包含 env 时,使用 redactEnvForLogs()。
Paperclip 技能注入

Paperclip 提供了一组共享技能(位于仓库顶层的 skills/ 目录中),供 Agent 在运行时使用,例如 paperclip API 技能和 paperclip-create-agent 工作流技能。每个适配器都要负责让 Agent 运行时能够发现这些技能,且不污染 Agent 的工作目录。

**约束:**绝不能将技能复制或符号链接到 Agent 的 cwd 中。cwd 是用户的项目检出目录——向其中写入 .claude/skills/ 或任何其他文件,都会把 Paperclip 内部文件带入仓库、破坏 git status,并可能导致这些文件被提交。

**模式:**为技能创建一个干净、隔离的位置,并告知 Agent 运行时到该位置查找。

claude-local 的做法:

  1. 在执行时创建一个新的临时目录:mkdtemp("paperclip-skills-")
  2. 在其中创建 .claude/skills/(Claude Code 预期的目录结构)
  3. 将仓库 skills/ 目录中的每个技能目录符号链接到临时目录的 .claude/skills/ 下
  4. 通过 --add-dir <tmpdir> 将临时目录传递给 Claude Code——这样 Claude Code 就会像这些技能注册在该目录中一样发现它们,同时不会触碰 Agent 实际的 cwd
  5. 在运行完成后,于 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 技能尚不存在,适配器就会将它们符号链接到该目录。这是可接受的,因为这是 Agent 工具自己的配置目录,而不是用户的项目。

// 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);
  }
}

**对于新适配器:**了解你的 Agent 运行时如何发现技能/插件,然后选择最干净的注入方式:

  1. 最佳:临时目录 + 标志(如 claude-local)——如果运行时支持“附加目录”标志,就创建一个临时目录,将技能符号链接到其中,传入该标志,并在之后清理。不会产生任何副作用。
  2. 可接受:全局配置目录(如 codex-local)——如果运行时有独立于项目的全局技能/插件目录,就将技能符号链接到那里。跳过已有条目,以免覆盖用户自定义内容。
  3. 可接受:环境变量——如果运行时通过环境变量读取技能/插件路径,就直接将其指向仓库的 skills/ 目录。
  4. 最后手段:注入提示词——如果运行时没有插件系统,就在提示词模板中包含技能内容。这样会消耗 token,但可以完全避免文件系统副作用。

**将技能作为按需加载的流程,而非提示词负担。**Paperclip 技能(如 paperclip 和 paperclip-create-agent)设计为按需调用的流程:Agent 会在上下文中看到技能元数据(名称 + 描述),但只有在决定调用某项技能时,才会加载完整的 SKILL.md 内容。这样可以缩短基础提示词。为适配器编写 agentConfigurationDoc 或提示词模板时,不要内联技能内容——让 Agent 运行时的技能发现机制来完成这项工作。每个 SKILL.md frontmatter 中的描述充当路由逻辑:它们告诉 Agent 何时加载完整技能,而不是说明技能包含什么。

**显式调用与模糊调用技能。**对于重视可靠性的生产工作流(例如必须始终调用 Paperclip API 报告状态的 Agent),请在提示词模板中使用明确指令:“使用 paperclip 技能报告进度。”模糊路由(让模型根据描述匹配自行决定)适用于探索性任务,但对于必须执行的流程并不可靠。


8. 安全注意事项

适配器处于 Paperclip 编排层与任意 Agent 执行之间的边界。这是一个高风险区域。

将 Agent 输出视为不可信内容

Agent 进程会运行由大模型驱动的代码,这些代码会读取外部文件、获取 URL 并执行工具。其输出可能受到所处理内容中的提示词注入影响。适配器的解析层是一个信任边界——验证所有内容,不执行任何内容。

通过环境变量注入密钥,而非提示词

切勿将密钥(API 密钥、令牌)放入提示词模板或会通过大模型传递的配置字段中。应将它们作为环境变量注入,供 Agent 的工具直接读取:

  • PAPERCLIP_API_KEY 由服务注入进程环境,而非提示词
  • 用户在 config.env 中提供的密钥会作为环境变量传递,并在 onMeta 日志中脱敏
  • redactEnvForLogs() 辅助函数会自动屏蔽任何匹配 /(key|token|secret|password|authorization|cookie)/i 的键

这遵循“sidecar 注入”模式:模型永远看不到真实的密钥值,但它调用的工具可以从环境中读取该值。

网络访问

如果 Agent 运行时支持网络访问控制(沙箱、允许列表),请在适配器中进行配置:

  • 优先使用最小化的允许列表,而非开放互联网访问。只需调用 Paperclip API 和 GitHub 的 Agent 不应访问任意主机。
  • 技能 + 网络 = 风险放大。教会 Agent 发出 HTTP 请求的技能,与不受限制的网络访问相结合,会形成数据外泄路径。请限制其中一项。
  • 如果运行时支持分层策略(组织级默认设置 + 每请求覆盖),请将组织级策略接入适配器配置,并允许每个 Agent 的配置进一步收紧限制。
进程隔离
  • 基于 CLI 的适配器会继承服务的用户权限。cwd 和 env 配置决定 Agent 进程可以访问文件系统中的哪些内容。
  • dangerouslySkipPermissions / dangerouslyBypassApprovalsAndSandbox 标志是为开发便利而设,但必须在 agentConfigurationDoc 中注明其危险性。生产部署不应使用这些标志。
  • 超时和宽限期(timeoutSec、graceSec)是安全保障措施,必须始终启用。失控的 Agent 进程若没有超时限制,可能会无限制地消耗资源。

9. TranscriptEntry 类型参考

UI 运行查看器会显示以下条目类型:

类型字段用途
initmodel、sessionIdAgent 初始化
assistanttextAgent 文本响应
thinkingtextAgent 推理/思考
usertext用户消息
tool_callname、input工具调用
tool_resulttoolUseId、content、isError工具结果
resulttext、inputTokens、outputTokens、cachedTokens、costUsd、subtype、isError、errors包含用量信息的最终结果
stderrtext标准错误输出
systemtext系统消息
stdouttext原始标准输出回退

10. 测试

在 server/src/__tests__/<adapter-name>-adapter.test.ts 中创建测试。测试以下内容:

  1. 输出解析 — 将示例标准输出传入解析器,并验证结构化输出
  2. 未知会话检测 — 验证 is<Agent>UnknownSessionError 函数
  3. 配置构建 — 验证 buildConfig 会根据表单值生成正确的 adapterConfig
  4. 会话编解码器 — 验证序列化/反序列化后能够往返还原

11. 最小适配器检查清单

  • packages/adapters/<name>/package.json,包含四个导出(., ./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,用于 Agent 表单的 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 工作区中(如果尚未被 glob 覆盖)
  • 为解析、会话编解码和配置构建编写测试

评论

支持 Markdown