Skip to main content

External Adapters

Vagaris supports external adapter plugins that can be installed from npm packages or local directories. External adapters work exactly like built-in adapters — they execute agents, parse output, and render transcripts — but they live in their own package and don't require changes to Vagaris's source code.

Built-in vs External​

Built-inExternal
Source locationInside this repo's packages/adapters/Separate npm package or local directory
RegistrationHardcoded in three registriesLoaded at startup via plugin system
UI parserStatic import at build timeDynamically loaded from API (see UI Parser)
DistributionShips with VagarisPublished to npm or linked via file:
UpdatesRequires Vagaris releaseIndependent versioning

Quick Start​

Minimal Package Structure​

my-adapter/
package.json
tsconfig.json
src/
index.ts # Shared metadata (type, label, models)
server/
index.ts # createServerAdapter() factory
execute.ts # Core execution logic
parse.ts # Output parsing
test.ts # Environment diagnostics
ui-parser.ts # Self-contained UI transcript parser

package.json​

{
"name": "my-vagris-adapter",
"version": "1.0.0",
"type": "module",
"license": "MIT",
"paperclip": {
"adapterUiParser": "1.0.0"
},
"exports": {
".": "./dist/index.js",
"./server": "./dist/server/index.js",
"./ui-parser": "./dist/ui-parser.js"
},
"files": ["dist"],
"scripts": {
"build": "tsc"
},
"dependencies": {
"@vagarylabs/adapter-utils": "^2026.325.0",
"picocolors": "^1.1.0"
},
"devDependencies": {
"@types/node": "^22.0.0",
"typescript": "^5.7.0"
}
}

Key fields:

FieldPurpose
exports["."]Entry point — must export createServerAdapter
exports["./ui-parser"]Self-contained UI parser module (optional but recommended)
paperclip.adapterUiParserContract version for the UI parser ("1.0.0")
filesLimits what gets published — only dist/

tsconfig.json​

{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "dist",
"rootDir": "src",
"declaration": true,
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src"]
}

Server Module​

The plugin loader calls createServerAdapter() from your package root. This function must return a ServerAdapterModule.

src/index.ts​

export const type = "my_adapter";     // snake_case, globally unique
export const label = "My Agent (local)";

export const models = [
{ id: "model-a", label: "Model A" },
];

export const agentConfigurationDoc = `# my_adapter configuration
Use when: ...
Don't use when: ...
`;

// Required by plugin-loader convention
export { createServerAdapter } from "./server/index.js";

src/server/index.ts​

import type { ServerAdapterModule } from "@vagarylabs/adapter-utils";
import { type, models, agentConfigurationDoc } from "../index.js";
import { execute } from "./execute.js";
import { testEnvironment } from "./test.js";

export function createServerAdapter(): ServerAdapterModule {
return {
type,
execute,
testEnvironment,
models,
agentConfigurationDoc,
};
}

src/server/execute.ts​

The core execution function. Receives an AdapterExecutionContext and returns an AdapterExecutionResult.

import type {
AdapterExecutionContext,
AdapterExecutionResult,
} from "@vagarylabs/adapter-utils";

import {
runChildProcess,
buildPaperclipEnv,
renderTemplate,
} from "@vagarylabs/adapter-utils/server-utils";

export async function execute(
ctx: AdapterExecutionContext,
): Promise<AdapterExecutionResult> {
const { config, agent, runtime, context, onLog, onMeta } = ctx;

// 1. Read config with safe helpers
const cwd = String(config.cwd ?? "/tmp");
const command = String(config.command ?? "my-agent");
const timeoutSec = Number(config.timeoutSec ?? 300);

// 2. Build environment with the PAPERCLIP_* vars injected
const env = buildPaperclipEnv(agent);

// 3. Render prompt template
const prompt = config.promptTemplate
? renderTemplate(String(config.promptTemplate), {
agentId: agent.id,
agentName: agent.name,
companyId: agent.companyId,
runId: ctx.runId,
taskId: context.taskId ?? "",
taskTitle: context.taskTitle ?? "",
})
: "Continue your work.";

// 4. Spawn process
const result = await runChildProcess(command, {
args: [prompt],
cwd,
env,
timeout: timeoutSec * 1000,
graceMs: 10_000,
onStdout: (chunk) => onLog("stdout", chunk),
onStderr: (chunk) => onLog("stderr", chunk),
});

// 5. Return structured result
return {
exitCode: result.exitCode,
timedOut: result.timedOut,
// Include session state for persistence
sessionParams: { /* ... */ },
};
}

Available Helpers from @vagris/adapter-utils​

HelperPurpose
runChildProcess(command, opts)Spawn a child process with timeout, grace period, and streaming callbacks
buildPaperclipEnv(agent)Inject PAPERCLIP_* environment variables
renderTemplate(template, data){{variable}} substitution in prompt templates
asString(v), asNumber(v), asBoolean(v)Safe config value extraction

src/server/test.ts​

Validates the adapter configuration before running. Returns structured diagnostics.

import type {
AdapterEnvironmentTestContext,
AdapterEnvironmentTestResult,
} from "@vagarylabs/adapter-utils";

export async function testEnvironment(
ctx: AdapterEnvironmentTestContext,
): Promise<AdapterEnvironmentTestResult> {
const checks = [];

// Example: check CLI is installed
checks.push({
level: "info",
message: "My Agent CLI v1.2.0 detected",
code: "cli_detected",
});

// Example: check working directory
const cwd = String(ctx.config.cwd ?? "");
if (!cwd.startsWith("/")) {
checks.push({
level: "error",
message: `Working directory must be absolute: "${cwd}"`,
hint: "Use /home/user/project or /workspace",
code: "invalid_cwd",
});
}

return {
adapterType: ctx.adapterType,
status: checks.some(c => c.level === "error") ? "fail" : "pass",
checks,
testedAt: new Date().toISOString(),
};
}

Check levels:

LevelMeaningEffect
infoInformationalShown in test results
warnNon-blocking issueShown with yellow indicator
errorBlocks executionPrevents agent from running

Installation​

From npm​

# Via the Vagaris UI
# Settings → Adapters → Install from npm → "my-vagris-adapter"

# Or via API
curl -X POST http://localhost:3102/api/adapters \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"packageName": "my-vagris-adapter"}'

From local directory​

curl -X POST http://localhost:3102/api/adapters \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"localPath": "/home/user/my-adapter"}'

Local adapters are symlinked into Vagaris's adapter directory. Changes to the source are picked up on server restart.

Via adapter-plugins.json​

For development, you can also edit ~/.paperclip/adapter-plugins.json directly:

[
{
"packageName": "my-vagris-adapter",
"localPath": "/home/user/my-adapter",
"type": "my_adapter",
"installedAt": "2026-03-30T12:00:00.000Z"
}
]

Optional: Session Persistence​

If your agent runtime supports sessions (conversation continuity across heartbeats), implement a session codec:

import type { AdapterSessionCodec } from "@vagarylabs/adapter-utils";

export const sessionCodec: AdapterSessionCodec = {
deserialize(raw) {
if (typeof raw !== "object" || raw === null) return null;
const r = raw as Record<string, unknown>;
return r.sessionId ? { sessionId: String(r.sessionId) } : null;
},
serialize(params) {
return params?.sessionId ? { sessionId: String(params.sessionId) } : null;
},
getDisplayId(params) {
return params?.sessionId ? String(params.sessionId) : null;
},
};

Include it in createServerAdapter():

return { type, execute, testEnvironment, sessionCodec, /* ... */ };

Optional: Skills Sync​

If your agent runtime supports skills/plugins, implement listSkills and syncSkills:

return {
type,
execute,
testEnvironment,
async listSkills(ctx) {
return {
adapterType: ctx.adapterType,
supported: true,
mode: "ephemeral",
desiredSkills: [],
entries: [],
warnings: [],
};
},
async syncSkills(ctx, desiredSkills) {
// Install desired skills into the runtime
return { /* same shape as listSkills */ };
},
};

Optional: Model Detection​

If your runtime has a local config file that specifies the default model:

async function detectModel() {
// Read ~/.my-agent/config.yaml or similar
return {
model: "anthropic/claude-sonnet-4",
provider: "anthropic",
source: "~/.my-agent/config.yaml",
candidates: ["anthropic/claude-sonnet-4", "openai/gpt-4o"],
};
}

return { type, execute, testEnvironment, detectModel: () => detectModel() };

Publishing​

npm run build
npm publish

Other Vagaris users can then install your adapter by package name from the UI or API.

Security​

  • Treat agent output as untrusted — parse defensively, never eval() agent output
  • Inject secrets via environment variables, not in prompts
  • Configure network access controls if the runtime supports them
  • Always enforce timeout and grace period — don't let agents run forever
  • The UI parser module runs in a browser sandbox — it must have zero runtime imports and no side effects

Next Steps​