create-agent-adapter

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Creating a Paperclip Agent Adapter

构建Paperclip Agent适配器

An adapter bridges Paperclip's orchestration layer to a specific AI agent runtime (Claude Code, Codex CLI, a custom process, an HTTP endpoint, etc.). Each adapter is a self-contained package that provides implementations for three consumers: the server, the UI, and the CLI.

适配器是连接Paperclip编排层与特定AI Agent运行时(如Claude Code、Codex CLI、自定义流程、HTTP端点等)的桥梁。每个适配器都是一个独立的包,可为三类使用者提供实现:服务端、UI界面和CLI命令行工具。

1. Architecture Overview

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
Three separate registries consume adapter modules:
RegistryLocationInterface
Server
server/src/adapters/registry.ts
ServerAdapterModule
UI
ui/src/adapters/registry.ts
UIAdapterModule
CLI
cli/src/adapters/registry.ts
CLIAdapterModule

packages/adapters/<name>/
  src/
    index.ts            # 共享元数据(类型、标签、模型、agentConfigurationDoc)
    server/
      index.ts          # 服务端导出:execute、sessionCodec、解析工具
      execute.ts        # 核心执行逻辑(AdapterExecutionContext -> AdapterExecutionResult)
      parse.ts          # 适配Agent输出格式的标准输出/结果解析器
    ui/
      index.ts          # UI导出:parseStdoutLine、buildConfig
      parse-stdout.ts   # 将逐行标准输出转换为运行查看器所需的TranscriptEntry[]
      build-config.ts   # 从CreateConfigValues生成Agent创建表单所需的adapterConfig JSON
    cli/
      index.ts          # CLI导出:formatStdoutEvent
      format-event.ts   # 为`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. Shared Types (
@paperclipai/adapter-utils
)

2. 共享类型(
@paperclipai/adapter-utils

All adapter interfaces live in
packages/adapter-utils/src/types.ts
. Import from
@paperclipai/adapter-utils
(types) or
@paperclipai/adapter-utils/server-utils
(runtime helpers).
所有适配器接口都位于
packages/adapter-utils/src/types.ts
中。可从
@paperclipai/adapter-utils
导入类型定义,或从
@paperclipai/adapter-utils/server-utils
导入运行时工具函数。

Core Interfaces

核心接口

ts
// 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;
}
ts
// 每个适配器必须实现的execute函数签名
interface AdapterExecutionContext {
  runId: string;
  agent: AdapterAgent;          // { id, companyId, name, adapterType, adapterConfig }
  runtime: AdapterRuntime;      // { sessionId, sessionParams, sessionDisplayId, taskKey }
  config: Record<string, unknown>;  // Agent的adapterConfig配置对象
  context: Record<string, unknown>; // 运行时上下文(taskId、wakeReason、approvalId等)
  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;      // 遗留字段 — 优先使用sessionParams
  sessionParams?: Record<string, unknown> | null;  // 运行间持久化的不透明会话状态
  sessionDisplayId?: string | null;
  provider?: string | null;       // "anthropic"、"openai"等
  model?: string | null;
  costUsd?: number | null;
  resultJson?: Record<string, unknown> | null;
  summary?: string | null;        // 人类可读的Agent执行摘要
  clearSession?: boolean;         // true = 通知Paperclip清除存储的会话
}

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

Module Interfaces

模块接口

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

ts
// 服务端 — 在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 — 在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 — 在cli/src/adapters/registry.ts中注册
interface CLIAdapterModule {
  type: string;
  formatStdoutEvent: (line: string, debug: boolean) => void;
}

2.1 Adapter Environment Test Contract

2.1 适配器环境测试约定

Every server adapter must implement
testEnvironment(...)
. This powers the board UI "Test environment" button in agent configuration.
ts
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
}
Guidelines:
  • Return structured diagnostics, never throw for expected findings.
  • Use
    error
    for invalid/unusable runtime setup (bad cwd, missing command, invalid URL).
  • Use
    warn
    for non-blocking but important situations.
  • Use
    info
    for successful checks and context.
Severity policy is product-critical: warnings are not save blockers.
Example: for
claude_local
, detected
ANTHROPIC_API_KEY
must be a
warn
, not an
error
, because Claude can still run (it just uses API-key auth instead of subscription auth).

每个服务端适配器都必须实现
testEnvironment(...)
方法,它为控制台UI中Agent配置的“测试环境”按钮提供功能支持。
ts
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时间戳
}

interface AdapterEnvironmentTestContext {
  companyId: string;
  adapterType: string;
  config: Record<string, unknown>; // 运行时解析后的adapterConfig
}
指导原则:
  • 返回结构化的诊断信息,对于预期的检测结果不要抛出异常。
  • 使用
    error
    级别标记无效/不可用的运行时配置(如错误的工作目录、缺失的命令、无效的URL)。
  • 使用
    warn
    级别标记非阻塞但重要的情况。
  • 使用
    info
    级别标记成功的检查和上下文信息。
严重程度规则对产品至关重要:警告不会阻止保存操作。 示例:对于
claude_local
适配器,检测到
ANTHROPIC_API_KEY
时应标记为
warn
而非
error
,因为Claude仍可运行(只是使用API密钥认证而非订阅认证)。

3. Step-by-Step: Creating a New Adapter

3. 分步指南:创建新适配器

3.1 Create the Package

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 — must use the four-export convention:
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"
  }
}
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 — 必须遵循四导出约定:
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 Root
index.ts
— Adapter Metadata

3.2 根目录
index.ts
— 适配器元数据

This file is imported by all three consumers (server, UI, CLI). Keep it dependency-free (no Node APIs, no React).
ts
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...
`;
Required exports:
  • type
    — the adapter type key, stored in
    agents.adapter_type
  • label
    — human-readable name for the UI
  • models
    — available model options for the agent creation form
  • agentConfigurationDoc
    — markdown describing all
    adapterConfig
    fields (used by LLM agents configuring other agents)
Writing
agentConfigurationDoc
as routing logic:
The
agentConfigurationDoc
is read by LLM agents (including Paperclip agents that create other agents). Write it as routing logic, not marketing copy. Include concrete "use when" and "don't use when" guidance so an LLM can decide whether this adapter is appropriate for a given task.
ts
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
...
`;
Adding explicit negative cases improves adapter selection accuracy. One concrete anti-pattern is worth more than three paragraphs of description.
该文件会被所有三类使用者(服务端、UI、CLI)导入。请保持无依赖(不使用Node API、不引入React)。
ts
export const type = "my_agent";        // 蛇形命名,全局唯一
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配置
...在此处记录所有配置字段...
`;
必填导出项:
  • type
    — 适配器类型标识,会存储在
    agents.adapter_type
    字段中
  • label
    — UI界面中显示的人类可读名称
  • models
    — Agent创建表单中可选的模型列表
  • agentConfigurationDoc
    — 描述所有
    adapterConfig
    字段的Markdown文档(供配置其他Agent的LLM Agent使用)
agentConfigurationDoc
编写为路由逻辑:
agentConfigurationDoc
会被LLM Agent读取(包括创建其他Agent的Paperclip Agent)。请将其编写为路由逻辑,而非营销文案。包含具体的“适用场景”和“不适用场景”指导,以便LLM能针对给定任务判断该适配器是否合适。
ts
export const agentConfigurationDoc = `# my_agent agent配置

适配器:my_agent

适用场景:
- 需要在本地主机运行MyAgent CLI的Agent
- 需要在多次运行间保持会话持久化(MyAgent支持线程恢复)
- 任务需要使用MyAgent专属工具(如网页搜索、代码执行)

不适用场景:
- 仅需简单的一次性脚本执行(请使用"process"适配器)
- Agent无需在多次运行间保留对话上下文(process适配器更简单)
- 主机未安装MyAgent CLI

核心字段:
- cwd(字符串,必填):Agent进程的绝对工作目录
...
`;
添加明确的负面案例可提高适配器选择的准确性。一个具体的反模式示例比三段描述更有价值。

3.3 Server Module

3.3 服务端模块

server/execute.ts
— The Core

server/execute.ts
— 核心逻辑

This is the most important file. It receives an
AdapterExecutionContext
and must return an
AdapterExecutionResult
.
Required behavior:
  1. Read config — extract typed values from
    ctx.config
    using helpers (
    asString
    ,
    asNumber
    ,
    asBoolean
    ,
    asStringArray
    ,
    parseObject
    from
    @paperclipai/adapter-utils/server-utils
    )
  2. Build environment — call
    buildPaperclipEnv(agent)
    then layer in
    PAPERCLIP_RUN_ID
    , context vars (
    PAPERCLIP_TASK_ID
    ,
    PAPERCLIP_WAKE_REASON
    ,
    PAPERCLIP_WAKE_COMMENT_ID
    ,
    PAPERCLIP_APPROVAL_ID
    ,
    PAPERCLIP_APPROVAL_STATUS
    ,
    PAPERCLIP_LINKED_ISSUE_IDS
    ), user env overrides, and auth token
  3. Resolve session — check
    runtime.sessionParams
    /
    runtime.sessionId
    for an existing session; validate it's compatible (e.g. same cwd); decide whether to resume or start fresh
  4. Render prompt — use
    renderTemplate(template, data)
    with the template variables:
    agentId
    ,
    companyId
    ,
    runId
    ,
    company
    ,
    agent
    ,
    run
    ,
    context
  5. Call onMeta — emit adapter invocation metadata before spawning the process
  6. Spawn the process — use
    runChildProcess()
    for CLI-based agents or
    fetch()
    for HTTP-based agents
  7. Parse output — convert the agent's stdout into structured data (session id, usage, summary, errors)
  8. Handle session errors — if resume fails with "unknown session", retry with a fresh session and set
    clearSession: true
  9. Return AdapterExecutionResult — populate all fields the agent runtime supports
Environment variables the server always injects:
VariableSource
PAPERCLIP_AGENT_ID
agent.id
PAPERCLIP_COMPANY_ID
agent.companyId
PAPERCLIP_API_URL
Server's own URL
PAPERCLIP_RUN_ID
Current run id
PAPERCLIP_TASK_ID
context.taskId
or
context.issueId
PAPERCLIP_WAKE_REASON
context.wakeReason
PAPERCLIP_WAKE_COMMENT_ID
context.wakeCommentId
or
context.commentId
PAPERCLIP_APPROVAL_ID
context.approvalId
PAPERCLIP_APPROVAL_STATUS
context.approvalStatus
PAPERCLIP_LINKED_ISSUE_IDS
context.issueIds
(comma-separated)
PAPERCLIP_API_KEY
authToken
(if no explicit key in config)
这是最重要的文件。它接收
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
    是否存在现有会话;验证其兼容性(如相同的工作目录);决定是恢复会话还是重新开始
  4. 渲染提示词 — 使用
    renderTemplate(template, data)
    ,模板变量包括:
    agentId
    companyId
    runId
    company
    agent
    run
    context
  5. 调用onMeta — 在启动进程前发送适配器调用元数据
  6. 启动进程 — 基于CLI的Agent使用
    runChildProcess()
    ,基于HTTP的Agent使用
    fetch()
  7. 解析输出 — 将Agent的标准输出转换为结构化数据(会话ID、使用量、摘要、错误信息)
  8. 处理会话错误 — 如果恢复会话时出现“未知会话”错误,则重新尝试创建新会话,并设置
    clearSession: true
  9. 返回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
— Output Parser

server/parse.ts
— 输出解析器

Parse the agent's stdout format into structured data. Must handle:
  • Session identification — extract session/thread ID from init events
  • Usage tracking — extract token counts (input, output, cached)
  • Cost tracking — extract cost if available
  • Summary extraction — pull the agent's final text response
  • Error detection — identify error states, extract error messages
  • Unknown session detection — export an
    is<Agent>UnknownSessionError()
    function for retry logic
Treat agent output as untrusted. The stdout you're parsing comes from an LLM-driven process that may have executed arbitrary tool calls, fetched external content, or been influenced by prompt injection in the files it read. Parse defensively:
  • Never
    eval()
    or dynamically execute anything from output
  • Use safe extraction helpers (
    asString
    ,
    asNumber
    ,
    parseJson
    ) — they return fallbacks on unexpected types
  • Validate session IDs and other structured data before passing them through
  • If output contains URLs, file paths, or commands, do not act on them in the adapter — just record them
将Agent的标准输出格式转换为结构化数据。必须处理:
  • 会话识别 — 从初始化事件中提取会话/线程ID
  • 使用量跟踪 — 提取令牌计数(输入、输出、缓存)
  • 成本跟踪 — 提取可用的成本信息
  • 摘要提取 — 提取Agent的最终文本响应
  • 错误检测 — 识别错误状态,提取错误信息
  • 未知会话检测 — 导出
    is<Agent>UnknownSessionError()
    函数用于重试逻辑
将Agent输出视为不可信内容。 你解析的标准输出来自LLM驱动的进程,它可能执行了任意工具调用、获取了外部内容,或受到了所读取文件中的提示注入影响。适配器的解析层是信任边界 — 请验证所有内容,不要执行任何代码。
  • 永远不要
    eval()
    或动态执行输出中的任何内容
  • 使用安全的提取工具(
    asString
    asNumber
    parseJson
    ) — 它们在遇到意外类型时会返回回退值
  • 在传递会话ID和其他结构化数据前进行验证
  • 如果输出包含URL、文件路径或命令,不要在适配器中执行它们 — 仅做记录即可

server/index.ts
— Server Exports

server/index.ts
— 服务端导出项

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 */ },
};
ts
export { execute } from "./execute.js";
export { testEnvironment } from "./test.js";
export { parseMyAgentOutput, isMyAgentUnknownSessionError } from "./parse.js";

// 会话编解码器 — 会话持久化必填
export const sessionCodec: AdapterSessionCodec = {
  deserialize(raw) { /* 原始DB JSON -> 类型化参数或null */ },
  serialize(params) { /* 类型化参数 -> 用于DB存储的JSON */ },
  getDisplayId(params) { /* -> 人类可读的会话ID字符串 */ },
};

server/test.ts
— Environment Diagnostics

server/test.ts
— 环境诊断

Implement adapter-specific preflight checks used by the UI test button.
Minimum expectations:
  1. Validate required config primitives (paths, commands, URLs, auth assumptions)
  2. Return check objects with deterministic
    code
    values
  3. Map severity consistently (
    info
    /
    warn
    /
    error
    )
  4. Compute final status:
    • fail
      if any
      error
    • warn
      if no errors and at least one warning
    • pass
      otherwise
This operation should be lightweight and side-effect free.
实现适配器专属的预检检查,供UI测试按钮使用。
最低要求:
  1. 验证必填的配置原语(路径、命令、URL、认证假设)
  2. 返回带有确定性
    code
    值的检查对象
  3. 一致地映射严重程度(
    info
    /
    warn
    /
    error
  4. 计算最终状态:
    • 若存在任何
      error
      ,则状态为
      fail
    • 若无错误但至少有一个警告,则状态为
      warn
    • 否则状态为
      pass
该操作应轻量且无副作用。

3.4 UI Module

3.4 UI模块

ui/parse-stdout.ts
— Transcript Parser

ui/parse-stdout.ts
— 会话记录解析器

Converts individual stdout lines into
TranscriptEntry[]
for the run detail viewer. Must handle the agent's streaming output format and produce entries of these kinds:
  • init
    — model/session initialization
  • assistant
    — agent text responses
  • thinking
    — agent thinking/reasoning (if supported)
  • tool_call
    — tool invocations with name and input
  • tool_result
    — tool results with content and error flag
  • user
    — user messages in the conversation
  • result
    — final result with usage stats
  • stdout
    — fallback for unparseable lines
ts
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
}
将单个标准输出行转换为运行详情查看器所需的
TranscriptEntry[]
。必须处理Agent的流式输出格式,并生成以下类型的条目:
  • init
    — 模型/会话初始化
  • assistant
    — Agent的文本响应
  • thinking
    — Agent的思考/推理过程(若支持)
  • tool_call
    — 工具调用,包含名称和输入参数
  • tool_result
    — 工具执行结果,包含内容和错误标记
  • user
    — 对话中的用户消息
  • result
    — 最终结果,包含使用量统计
  • stdout
    — 无法解析的行的回退类型
ts
export function parseMyAgentStdoutLine(line: string, ts: string): TranscriptEntry[] {
  // 解析JSON行,映射为对应的TranscriptEntry类型
  // 回退返回[{ kind: "stdout", ts, text: line }]
}

ui/build-config.ts
— Config Builder

ui/build-config.ts
— 配置构建器

Converts the UI form's
CreateConfigValues
into the
adapterConfig
JSON blob stored on the agent.
ts
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表单的
CreateConfigValues
转换为存储在Agent中的
adapterConfig
JSON对象。
ts
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;
  // ... 适配器专属字段
  return ac;
}

UI Config Fields Component

UI配置字段组件

Create
ui/src/adapters/<name>/config-fields.tsx
with a React component implementing
AdapterConfigFieldsProps
. This renders adapter-specific form fields in the agent creation/edit form.
Use the shared primitives from
ui/src/components/agent-config-primitives
:
  • Field
    — labeled form field wrapper
  • ToggleField
    — boolean toggle with label and hint
  • DraftInput
    — text input with draft/commit behavior
  • DraftNumberInput
    — number input with draft/commit behavior
  • help
    — standard hint text for common fields
The component must support both
create
mode (using
values
/
set
) and
edit
mode (using
config
/
eff
/
mark
).
创建
ui/src/adapters/<name>/config-fields.tsx
React组件,实现
AdapterConfigFieldsProps
接口。该组件会在Agent创建/编辑表单中渲染适配器专属的表单字段。
使用
ui/src/components/agent-config-primitives
中的共享原语:
  • Field
    — 带标签的表单字段包装器
  • ToggleField
    — 带标签和提示的布尔开关
  • DraftInput
    — 带有草稿/提交机制的文本输入框
  • DraftNumberInput
    — 带有草稿/提交机制的数字输入框
  • help
    — 通用字段的标准提示文本
该组件必须同时支持
create
模式(使用
values
/
set
)和
edit
模式(使用
config
/
eff
/
mark
)。

3.5 CLI Module

3.5 CLI模块

cli/format-event.ts
— Terminal Formatter

cli/format-event.ts
— 终端格式化器

Pretty-prints stdout lines for
paperclipai run --watch
. Use
picocolors
for coloring.
ts
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
}

paperclipai run --watch
命令美化输出标准输出行。使用
picocolors
库添加颜色。
ts
import pc from "picocolors";

export function printMyAgentStreamEvent(raw: string, debug: boolean): void {
  // 解析Agent标准输出中的JSON行
  // 打印带颜色的输出:系统消息为蓝色、助手消息为绿色、工具相关为黄色
  // 在调试模式下,将无法识别的行显示为灰色
}

4. Registration Checklist

4. 注册检查清单

After creating the adapter package, register it in all three consumers:
创建适配器包后,需在三类使用者中分别注册:

4.1 Server Registry (
server/src/adapters/registry.ts
)

4.1 服务端注册器(
server/src/adapters/registry.ts

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]),
);
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,  // 若Agent可使用Paperclip API则设为true
  agentConfigurationDoc: myDoc,
};

// 添加到adaptersByType映射中
const adaptersByType = new Map<string, ServerAdapterModule>(
  [..., myAgentAdapter].map((a) => [a.type, a]),
);

4.2 UI Registry (
ui/src/adapters/registry.ts
)

4.2 UI注册器(
ui/src/adapters/registry.ts

ts
import { myAgentUIAdapter } from "./my-agent";

const adaptersByType = new Map<string, UIAdapterModule>(
  [..., myAgentUIAdapter].map((a) => [a.type, a]),
);
With
ui/src/adapters/my-agent/index.ts
:
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,
};
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
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
)

4.3 CLI注册器(
cli/src/adapters/registry.ts

ts
import { printMyAgentStreamEvent } from "@paperclipai/adapter-my-agent/cli";

const myAgentCLIAdapter: CLIAdapterModule = {
  type: "my_agent",
  formatStdoutEvent: printMyAgentStreamEvent,
};

// Add to the adaptersByType map

ts
import { printMyAgentStreamEvent } from "@paperclipai/adapter-my-agent/cli";

const myAgentCLIAdapter: CLIAdapterModule = {
  type: "my_agent",
  formatStdoutEvent: printMyAgentStreamEvent,
};

// 添加到adaptersByType映射中

5. Session Management — Designing for Long Runs

5. 会话管理 — 为长时运行设计

Sessions allow agents to maintain conversation context across runs. The system is codec-based — each adapter defines how to serialize/deserialize its session state.
Design for long runs from the start. Treat session reuse as the default primitive, not an optimization to add later. An agent working on an issue may be woken dozens of times — for the initial assignment, approval callbacks, re-assignments, manual nudges. Each wake should resume the existing conversation so the agent retains full context about what it has already done, what files it has read, and what decisions it has made. Starting fresh each time wastes tokens on re-reading the same files and risks contradictory decisions.
Key concepts:
  • sessionParams
    is an opaque
    Record<string, unknown>
    stored in the DB per task
  • The adapter's
    sessionCodec.serialize()
    converts execution result data to storable params
  • sessionCodec.deserialize()
    converts stored params back for the next run
  • sessionCodec.getDisplayId()
    extracts a human-readable session ID for the UI
  • cwd-aware resume: if the session was created in a different cwd than the current config, skip resuming (prevents cross-project session contamination)
  • Unknown session retry: if resume fails with a "session not found" error, retry with a fresh session and return
    clearSession: true
    so Paperclip wipes the stale session
If the agent runtime supports any form of context compaction or conversation compression (e.g. Claude Code's automatic context management, or Codex's
previous_response_id
chaining), lean on it. Adapters that support session resume get compaction for free — the agent runtime handles context window management internally across resumes.
Pattern (from both claude-local and codex-local):
ts
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 });
}

会话允许Agent在多次运行间保持对话上下文。系统采用编解码器模式 — 每个适配器定义如何序列化/反序列化其会话状态。
从一开始就为长时运行设计。 将会话复用视为默认原语,而非后续添加的优化项。处理某个问题的Agent可能会被唤醒数十次 — 初始分配、审批回调、重新分配、手动触发。每次唤醒都应恢复现有对话,以便Agent保留已完成工作、已读取文件和已做决策的完整上下文。每次都重新开始会浪费令牌重复读取相同文件,还可能导致决策矛盾。
核心概念:
  • sessionParams
    是存储在数据库中的不透明
    Record<string, unknown>
    对象,按任务存储
  • 适配器的
    sessionCodec.serialize()
    将执行结果数据转换为可存储的参数
  • sessionCodec.deserialize()
    将存储的参数转换为下一次运行可用的格式
  • sessionCodec.getDisplayId()
    提取供UI显示的人类可读会话ID
  • 感知工作目录的恢复:如果会话创建时的工作目录与当前配置不同,则跳过恢复(防止跨项目会话污染)
  • 未知会话重试:如果恢复会话失败并出现“会话未找到”错误,则重新尝试创建新会话,并返回
    clearSession: true
    ,让Paperclip清除过期会话
如果Agent运行时支持任何形式的上下文压缩或对话压缩(如Claude Code的自动上下文管理,或Codex的
previous_response_id
链式调用),请充分利用。支持会话恢复的适配器可免费获得压缩功能 — Agent运行时会在多次恢复间自动处理上下文窗口管理。
模式(来自claude-local和codex-local适配器):
ts
const canResumeSession =
  runtimeSessionId.length > 0 &&
  (runtimeSessionCwd.length === 0 || path.resolve(runtimeSessionCwd) === path.resolve(cwd));
const sessionId = canResumeSession ? runtimeSessionId : null;

// ... 运行尝试 ...

// 如果恢复会话失败并出现未知会话错误,则重新尝试创建新会话
if (sessionId && !proc.timedOut && exitCode !== 0 && isUnknownSessionError(output)) {
  const retry = await runAttempt(null);
  return toResult(retry, { clearSessionOnMissingSession: true });
}

6. Server-Utils Helpers

6. Server-Utils工具函数

Import from
@paperclipai/adapter-utils/server-utils
:
HelperPurpose
asString(val, fallback)
Safe string extraction
asNumber(val, fallback)
Safe number extraction
asBoolean(val, fallback)
Safe boolean extraction
asStringArray(val)
Safe string array extraction
parseObject(val)
Safe
Record<string, unknown>
extraction
parseJson(str)
Safe JSON.parse returning
Record
or null
renderTemplate(tmpl, data)
{{path.to.value}}
template rendering
buildPaperclipEnv(agent)
Standard
PAPERCLIP_*
env vars
redactEnvForLogs(env)
Redact sensitive keys for onMeta
ensureAbsoluteDirectory(cwd)
Validate cwd exists and is absolute
ensureCommandResolvable(cmd, cwd, env)
Validate command is in PATH
ensurePathInEnv(env)
Ensure PATH exists in env
runChildProcess(runId, cmd, args, opts)
Spawn with timeout, logging, capture

可从
@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)
为onLog日志脱敏敏感密钥
ensureAbsoluteDirectory(cwd)
验证工作目录存在且为绝对路径
ensureCommandResolvable(cmd, cwd, env)
验证命令在PATH中可找到
ensurePathInEnv(env)
确保环境变量中存在PATH
runChildProcess(runId, cmd, args, opts)
带超时、日志和捕获功能的进程启动函数

7. Conventions and Patterns

7. 约定与模式

Naming

命名规范

  • Adapter type:
    snake_case
    (e.g.
    claude_local
    ,
    codex_local
    )
  • Package name:
    @paperclipai/adapter-<kebab-name>
  • Package directory:
    packages/adapters/<kebab-name>/
  • 适配器类型:使用蛇形命名(如
    claude_local
    codex_local
  • 包名:
    @paperclipai/adapter-<短横线命名>
  • 包目录:
    packages/adapters/<短横线命名>/

Config Parsing

配置解析

  • Never trust
    config
    values directly — always use
    asString
    ,
    asNumber
    , etc.
  • Provide sensible defaults for every optional field
  • Document all fields in
    agentConfigurationDoc
  • 永远不要直接信任
    config
    值 — 始终使用
    asString
    asNumber
    等工具函数
  • 为每个可选字段提供合理的默认值
  • agentConfigurationDoc
    中记录所有字段

Prompt Templates

提示词模板

  • Support
    promptTemplate
    for every run
  • Use
    renderTemplate()
    with the standard variable set
  • Default prompt:
    "You are agent {{agent.id}} ({{agent.name}}). Continue your Paperclip work."
  • 为每次运行支持
    promptTemplate
  • 使用
    renderTemplate()
    和标准变量集
  • 默认提示词:
    "You are agent {{agent.id}} ({{agent.name}}). Continue your Paperclip work."

Error Handling

错误处理

  • Differentiate timeout vs process error vs parse failure
  • Always populate
    errorMessage
    on failure
  • Include raw stdout/stderr in
    resultJson
    when parsing fails
  • Handle the agent CLI not being installed (command not found)
  • 区分超时、进程错误和解析失败
  • 失败时始终填充
    errorMessage
    字段
  • 解析失败时将原始标准输出/错误输出包含在
    resultJson
  • 处理Agent CLI未安装的情况(命令未找到)

Logging

日志记录

  • Call
    onLog("stdout", ...)
    and
    onLog("stderr", ...)
    for all process output — this feeds the real-time run viewer
  • Call
    onMeta(...)
    before spawning to record invocation details
  • Use
    redactEnvForLogs()
    when including env in meta
  • 为所有进程输出调用
    onLog("stdout", ...)
    onLog("stderr", ...)
    — 这会为实时运行查看器提供数据
  • 启动进程前调用
    onMeta(...)
    记录调用详情
  • 在元数据中包含环境变量时使用
    redactEnvForLogs()

Paperclip Skills Injection

Paperclip技能注入

Paperclip ships shared skills (in the repo's top-level
skills/
directory) that agents need at runtime — things like the
paperclip
API skill and the
paperclip-create-agent
workflow skill. Each adapter is responsible for making these skills discoverable by its agent runtime without polluting the agent's working directory.
The constraint: never copy or symlink skills into the agent's
cwd
. The cwd is the user's project checkout — writing
.claude/skills/
or any other files into it would contaminate the repo with Paperclip internals, break git status, and potentially leak into commits.
The pattern: create a clean, isolated location for skills and tell the agent runtime to look there.
How claude-local does it:
  1. At execution time, create a fresh tmpdir:
    mkdtemp("paperclip-skills-")
  2. Inside it, create
    .claude/skills/
    (the directory structure Claude Code expects)
  3. Symlink each skill directory from the repo's
    skills/
    into the tmpdir's
    .claude/skills/
  4. Pass the tmpdir to Claude Code via
    --add-dir <tmpdir>
    — this makes Claude Code discover the skills as if they were registered in that directory, without touching the agent's actual cwd
  5. Clean up the tmpdir in a
    finally
    block after the run completes
ts
// 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 })
How codex-local does it:
Codex has a global personal skills directory (
$CODEX_HOME/skills
or
~/.codex/skills
). The adapter symlinks Paperclip skills there if they don't already exist. This is acceptable because it's the agent tool's own config directory, not the user's project.
ts
// 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);
  }
}
For a new adapter: figure out how your agent runtime discovers skills/plugins, then choose the cleanest injection path:
  1. Best: tmpdir + flag (like claude-local) — if the runtime supports an "additional directory" flag, create a tmpdir, symlink skills in, pass the flag, clean up after. Zero side effects.
  2. Acceptable: global config dir (like codex-local) — if the runtime has a global skills/plugins directory separate from the project, symlink there. Skip existing entries to avoid overwriting user customizations.
  3. Acceptable: env var — if the runtime reads a skills/plugin path from an environment variable, point it at the repo's
    skills/
    directory directly.
  4. Last resort: prompt injection — if the runtime has no plugin system, include skill content in the prompt template itself. This uses tokens but avoids filesystem side effects entirely.
Skills as loaded procedures, not prompt bloat. The Paperclip skills (like
paperclip
and
paperclip-create-agent
) are designed as on-demand procedures: the agent sees skill metadata (name + description) in its context, but only loads the full SKILL.md content when it decides to invoke a skill. This keeps the base prompt small. When writing
agentConfigurationDoc
or prompt templates for your adapter, do not inline skill content — let the agent runtime's skill discovery do the work. The descriptions in each SKILL.md frontmatter act as routing logic: they tell the agent when to load the full skill, not what the skill contains.
Explicit vs. fuzzy skill invocation. For production workflows where reliability matters (e.g. an agent that must always call the Paperclip API to report status), use explicit instructions in the prompt template: "Use the paperclip skill to report your progress." Fuzzy routing (letting the model decide based on description matching) is fine for exploratory tasks but unreliable for mandatory procedures.

Paperclip在仓库顶级
skills/
目录中提供了Agent运行时所需的共享技能 — 比如
paperclip
API技能和
paperclip-create-agent
工作流技能。每个适配器都负责让这些技能被其Agent运行时发现,同时不污染Agent的工作目录
约束条件: 永远不要将技能复制或链接到Agent的
cwd
目录中。工作目录是用户的项目检出目录 — 在其中写入
.claude/skills/
或其他文件会污染仓库、破坏git状态,甚至可能被提交到版本库中。
解决方案: 创建一个干净的隔离位置存放技能,并告知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的实际工作目录
  5. 运行完成后在
    finally
    块中清理临时目录
ts
// 来自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;
}

// 在execute()中:将--add-dir参数传递给Claude Code
const skillsDir = await buildSkillsDir();
args.push("--add-dir", skillsDir);
// ... 运行进程 ...
// 在finally块中:fs.rm(skillsDir, { recursive: true, force: true })
codex-local的实现方式:
Codex有一个全局的个人技能目录(
$CODEX_HOME/skills
~/.codex/skills
)。适配器会将Paperclip技能链接到该目录(如果不存在的话)。这是可接受的,因为这是Agent工具自身的配置目录,而非用户的项目目录。
ts
// 来自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;  // 不要覆盖用户自定义的技能
    await fs.symlink(source, target);
  }
}
对于新适配器: 了解你的Agent运行时如何发现技能/插件,然后选择最干净的注入方式:
  1. 最佳方案:临时目录+参数(如claude-local) — 如果运行时支持“附加目录”参数,创建临时目录、链接技能、传递参数、运行后清理。无任何副作用。
  2. 可接受方案:全局配置目录(如codex-local) — 如果运行时有独立于项目的全局技能/插件目录,可将技能链接到该目录。跳过已存在的条目以避免覆盖用户自定义内容。
  3. 可接受方案:环境变量 — 如果运行时从环境变量读取技能/插件路径,直接将其指向仓库的
    skills/
    目录。
  4. 最后方案:提示词注入 — 如果运行时没有插件系统,可将技能内容包含在提示词模板中。这会消耗令牌,但完全避免了文件系统副作用。
技能是加载的程序,而非提示词膨胀。 Paperclip技能(如
paperclip
paperclip-create-agent
)被设计为按需加载的程序:Agent在上下文中看到技能元数据(名称+描述),但仅在决定调用技能时才加载完整的SKILL.md内容。这能保持基础提示词的简洁性。为适配器编写
agentConfigurationDoc
或提示词模板时,不要内联技能内容 — 让Agent运行时的技能发现机制来处理。每个SKILL.md前置元数据中的描述充当路由逻辑:告诉Agent何时加载完整技能,而非技能的具体内容。
显式与模糊技能调用。 对于可靠性要求高的生产工作流(如必须调用Paperclip API报告状态的Agent),请在提示词模板中使用显式指令:“使用paperclip技能报告你的进度。” 模糊路由(让模型根据描述匹配决定)适用于探索性任务,但对于强制流程不可靠。

8. Security Considerations

8. 安全考量

Adapters sit at the boundary between Paperclip's orchestration layer and arbitrary agent execution. This is a high-risk surface.
适配器位于Paperclip编排层与任意Agent执行的边界,属于高风险区域。

Treat Agent Output as Untrusted

将Agent输出视为不可信内容

The agent process runs LLM-driven code that reads external files, fetches URLs, and executes tools. Its output may be influenced by prompt injection from the content it processes. The adapter's parse layer is a trust boundary — validate everything, execute nothing.
Agent进程运行LLM驱动的代码,会读取外部文件、获取URL并执行工具。其输出可能受到所处理内容中的提示注入影响。适配器的解析层是信任边界 — 验证所有内容,不要执行任何代码。

Secret Injection via Environment, Not Prompts

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

Never put secrets (API keys, tokens) into prompt templates or config fields that flow through the LLM. Instead, inject them as environment variables that the agent's tools can read directly:
  • PAPERCLIP_API_KEY
    is injected by the server into the process environment, not the prompt
  • User-provided secrets in
    config.env
    are passed as env vars, redacted in
    onMeta
    logs
  • The
    redactEnvForLogs()
    helper automatically masks any key matching
    /(key|token|secret|password|authorization|cookie)/i
This follows the "sidecar injection" pattern: the model never sees the real secret value, but the tools it invokes can read it from the environment.
永远不要将密钥(API密钥、令牌)放入提示词模板或会流经LLM的配置字段中。相反,应将它们作为环境变量注入,让Agent的工具直接读取:
  • PAPERCLIP_API_KEY
    由服务端注入到进程环境中,而非提示词
  • config.env
    中用户提供的密钥会作为环境变量传递,并在
    onMeta
    日志中脱敏
  • redactEnvForLogs()
    工具函数会自动屏蔽匹配
    /(key|token|secret|password|authorization|cookie)/i
    的密钥
这遵循“边车注入”模式:模型永远不会看到真实的密钥值,但它调用的工具可以从环境中读取。

Network Access

网络访问

If your agent runtime supports network access controls (sandboxing, allowlists), configure them in the adapter:
  • Prefer minimal allowlists over open internet access. An agent that only needs to call the Paperclip API and GitHub should not have access to arbitrary hosts.
  • Skills + network = amplified risk. A skill that teaches the agent to make HTTP requests combined with unrestricted network access creates an exfiltration path. Constrain one or the other.
  • If the runtime supports layered policies (org-level defaults + per-request overrides), wire the org-level policy into the adapter config and let per-agent config narrow further.
如果你的Agent运行时支持网络访问控制(沙箱、允许列表),请在适配器中配置:
  • 优先使用最小化的允许列表,而非开放互联网访问。仅需调用Paperclip API和GitHub的Agent不应有权访问任意主机。
  • 技能+网络=风险放大。教会Agent发起HTTP请求的技能加上无限制的网络访问会创建数据泄露路径。请约束其中一项或两项。
  • 如果运行时支持分层策略(组织级默认值+每次请求覆盖),请将组织级策略接入适配器配置,并允许每个Agent的配置进一步缩小范围。

Process Isolation

进程隔离

  • CLI-based adapters inherit the server's user permissions. The
    cwd
    and
    env
    config determine what the agent process can access on the filesystem.
  • dangerouslySkipPermissions
    /
    dangerouslyBypassApprovalsAndSandbox
    flags exist for development convenience but must be documented as dangerous in
    agentConfigurationDoc
    . Production deployments should not use them.
  • Timeout and grace period (
    timeoutSec
    ,
    graceSec
    ) are safety rails — always enforce them. A runaway agent process without a timeout can consume unbounded resources.

  • 基于CLI的适配器会继承服务端的用户权限。
    cwd
    env
    配置决定了Agent进程可访问的文件系统资源。
  • dangerouslySkipPermissions
    /
    dangerouslyBypassApprovalsAndSandbox
    标志仅用于开发便利,必须在
    agentConfigurationDoc
    中标记为危险。生产环境部署不应使用它们。
  • 超时和宽限期(
    timeoutSec
    graceSec
    )是安全防护措施 — 始终强制执行。没有超时的失控Agent进程会消耗无限资源。

9. TranscriptEntry Kinds Reference

9. TranscriptEntry类型参考

The UI run viewer displays these entry kinds:
KindFieldsUsage
init
model
,
sessionId
Agent initialization
assistant
text
Agent text response
thinking
text
Agent reasoning/thinking
user
text
User message
tool_call
name
,
input
Tool invocation
tool_result
toolUseId
,
content
,
isError
Tool result
result
text
,
inputTokens
,
outputTokens
,
cachedTokens
,
costUsd
,
subtype
,
isError
,
errors
Final result with usage
stderr
text
Stderr output
system
text
System messages
stdout
text
Raw stdout fallback

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. Testing

10. 测试

Create tests in
server/src/__tests__/<adapter-name>-adapter.test.ts
. Test:
  1. Output parsing — feed sample stdout through your parser, verify structured output
  2. Unknown session detection — verify the
    is<Agent>UnknownSessionError
    function
  3. Config building — verify
    buildConfig
    produces correct adapterConfig from form values
  4. Session codec — verify serialize/deserialize round-trips

server/src/__tests__/<adapter-name>-adapter.test.ts
中创建测试。测试内容包括:
  1. 输出解析 — 将示例标准输出传入解析器,验证结构化输出
  2. 未知会话检测 — 验证
    is<Agent>UnknownSessionError
    函数的正确性
  3. 配置构建 — 验证
    buildConfig
    从表单值生成正确的adapterConfig
  4. 会话编解码器 — 验证序列化/反序列化的往返一致性

11. Minimal Adapter Checklist

11. 最小适配器检查清单

  • packages/adapters/<name>/package.json
    with four exports (
    .
    ,
    ./server
    ,
    ./ui
    ,
    ./cli
    )
  • Root
    index.ts
    with
    type
    ,
    label
    ,
    models
    ,
    agentConfigurationDoc
  • server/execute.ts
    implementing
    AdapterExecutionContext -> AdapterExecutionResult
  • server/test.ts
    implementing
    AdapterEnvironmentTestContext -> AdapterEnvironmentTestResult
  • server/parse.ts
    with output parser and unknown-session detector
  • server/index.ts
    exporting
    execute
    ,
    testEnvironment
    ,
    sessionCodec
    , parse helpers
  • ui/parse-stdout.ts
    with
    StdoutLineParser
    for the run viewer
  • ui/build-config.ts
    with
    CreateConfigValues -> adapterConfig
    builder
  • ui/src/adapters/<name>/config-fields.tsx
    React component for agent form
  • ui/src/adapters/<name>/index.ts
    assembling the
    UIAdapterModule
  • cli/format-event.ts
    with terminal formatter
  • cli/index.ts
    exporting the formatter
  • Registered in
    server/src/adapters/registry.ts
  • Registered in
    ui/src/adapters/registry.ts
  • Registered in
    cli/src/adapters/registry.ts
  • Added to workspace in root
    pnpm-workspace.yaml
    (if not already covered by glob)
  • Tests for parsing, session codec, and config building
  • 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
    中添加到工作区(如果通配符未覆盖)
  • 包含解析、会话编解码器和配置构建的测试用例