关于 create-agent-adapter
面向 Paperclip 扩展开发者的 Agent Adapter 创建指南,说明如何连接不同 Agent 运行时。它涵盖服务器、UI 和 CLI 三个消费者所需的模块、诊断与注册流程。
主要功能
- 01定义服务器、UI、CLI 模块
- 02规范执行与会话处理
- 03解析输出并生成运行记录
- 04构建配置表单与环境诊断
- 05提供适配器注册检查清单
适用场景
- 01为新 Agent 运行时创建适配器
- 02添加 CLI 或 HTTP 运行逻辑
- 03实现适配器配置和环境预检
- 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.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(...)。此功能用于 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。
必需行为:
- 读取配置 — 使用辅助函数(来自
@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 是否相同);决定是恢复会话还是启动新会话 - 渲染提示词 — 使用
renderTemplate(template, data)和模板变量:agentId、companyId、runId、company、agent、run、context - 调用 onMeta — 在启动进程前发送适配器调用元数据
- 启动进程 — 对基于 CLI 的 Agent 使用
runChildProcess(),对基于 HTTP 的 Agent 使用fetch() - 解析输出 — 将 Agent 的 stdout 转换为结构化数据(会话 ID、用量、摘要、错误)
- 处理会话错误 — 如果恢复会话时因“unknown session”失败,则使用新会话重试并设置
clearSession: true - 返回 AdapterExecutionResult — 填充 Agent 运行时支持的所有字段
服务始终注入的环境变量:
| 变量 | 来源 |
|---|---|
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 — 输出解析器
将 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 测试按钮使用。
最低要求:
- 验证必需的配置基本项(路径、命令、URL、身份验证假设)
- 返回具有确定性
code值的检查对象 - 一致地映射严重级别(
info/warn/error) - 计算最终状态:
- 如果存在任何
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 的做法:
- 在执行时创建一个新的临时目录:
mkdtemp("paperclip-skills-") - 在其中创建
.claude/skills/(Claude Code 预期的目录结构) - 将仓库
skills/目录中的每个技能目录符号链接到临时目录的.claude/skills/下 - 通过
--add-dir <tmpdir>将临时目录传递给 Claude Code——这样 Claude Code 就会像这些技能注册在该目录中一样发现它们,同时不会触碰 Agent 实际的cwd - 在运行完成后,于
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 运行时如何发现技能/插件,然后选择最干净的注入方式:
- 最佳:临时目录 + 标志(如 claude-local)——如果运行时支持“附加目录”标志,就创建一个临时目录,将技能符号链接到其中,传入该标志,并在之后清理。不会产生任何副作用。
- 可接受:全局配置目录(如 codex-local)——如果运行时有独立于项目的全局技能/插件目录,就将技能符号链接到那里。跳过已有条目,以免覆盖用户自定义内容。
- 可接受:环境变量——如果运行时通过环境变量读取技能/插件路径,就直接将其指向仓库的
skills/目录。 - 最后手段:注入提示词——如果运行时没有插件系统,就在提示词模板中包含技能内容。这样会消耗 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 运行查看器会显示以下条目类型:
| 类型 | 字段 | 用途 |
|---|---|---|
init | model、sessionId | Agent 初始化 |
assistant | text | Agent 文本响应 |
thinking | text | Agent 推理/思考 |
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,包含四个导出(.,./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 覆盖) - 为解析、会话编解码和配置构建编写测试