Skip to content

Build an Agent

This tutorial walks through building a CKP-conformant agent at each conformance level. You will start with a minimal L1 agent and progressively add tools, security gates, memory, swarm coordination, and optional predictive planning metadata.

Terminal window
npm install @clawkernel/sdk

You will also need a claw.yaml manifest for each level. The SDK ships example manifests in sdk/examples/.


Step 1: L1 agent — Identity and Provider

Section titled “Step 1: L1 agent — Identity and Provider”

An L1 agent only needs a name and version. The SDK handles initialization, status, shutdown, heartbeat, and all required error responses automatically.

import { createAgent } from "@clawkernel/sdk";
const agent = createAgent({
name: "my-agent",
version: "1.0.0",
});
agent.listen();

That is the entire agent. It communicates over stdio using JSON-RPC 2.0 and passes all 13 L1 test vectors.

The corresponding claw.yaml declares the agent’s identity and at least one provider:

claw: "0.3.0"
kind: Claw
metadata:
name: my-agent
spec:
identity:
inline:
personality: "You are a helpful assistant."
autonomy: supervised
providers:
- inline:
protocol: openai-compatible
endpoint: http://localhost:11434/v1
model: llama3
auth:
type: none
  • claw.initialize / claw.initialized handshake
  • claw.status with lifecycle state tracking
  • claw.shutdown with graceful drain
  • claw.heartbeat periodic notifications
  • Proper JSON-RPC error handling (parse errors, invalid requests, unknown methods)

Step 2: L2 agent — Tools, Policy, and Sandbox

Section titled “Step 2: L2 agent — Tools, Policy, and Sandbox”

L2 adds executable tools with a five-gate security pipeline. Every tool call passes through quota, policy, sandbox, existence check, and approval before execution.

import { createAgent } from "@clawkernel/sdk";
const agent = createAgent({
name: "my-agent",
version: "1.0.0",
// Tools: register callable functions
tools: {
echo: {
execute: async (args) => ({
content: [{ type: "text", text: args.text as string }],
}),
},
"slow-tool": {
timeout_ms: 100, // Will timeout if execution exceeds 100ms
execute: async () => {
await new Promise((r) => setTimeout(r, 5000));
return { content: [{ type: "text", text: "done" }] };
},
},
},
// Policy gate: deny specific tools by name
policy: {
evaluate: (toolName) => {
if (toolName === "destructive-tool")
return { allowed: false, code: -32011 };
return { allowed: true };
},
},
// Sandbox gate: block dangerous arguments
sandbox: {
check: (_toolName, args) => {
if ((args.url as string)?.includes("169.254"))
return { allowed: false, code: -32010 };
return { allowed: true };
},
},
// Quota gate: reject expensive calls
quota: {
check: (toolName) => {
if (toolName === "expensive-tool")
return { allowed: false, code: -32021 };
return { allowed: true };
},
},
// Approval gate: require human approval for specific tools
approval: {
required: (toolName) => toolName === "deploy",
timeout_ms: 30000,
},
});
agent.listen();

The L2 manifest adds channels, tools, sandbox, and policies:

claw: "0.3.0"
kind: Claw
metadata:
name: my-agent
spec:
identity:
inline:
personality: "L2 agent with tools and security."
autonomy: supervised
providers:
- inline:
protocol: openai-compatible
endpoint: http://localhost:11434/v1
model: test-model
auth:
type: none
channels:
- inline:
type: cli
transport: stdio
auth:
secret_ref: CLI_TOKEN
tools:
- inline:
name: echo
description: "Echo input back"
input_schema:
type: object
properties:
text:
type: string
sandbox:
inline:
level: process
policies:
- inline:
rules:
- id: allow-all
action: allow
scope: all

Tool definition — Each tool has an execute function and an optional timeout_ms. The function receives parsed arguments and returns a content array.

Gate pipeline — The five gates run in order: quota -> policy -> sandbox -> exists -> approval -> execute. A tool denied by policy never reveals whether it exists.

Error codes — Each gate maps to a specific CKP error code: -32021 (quota), -32011 (policy), -32010 (sandbox), -32012 (approval timeout), -32013 (approval denied), -32014 (tool timeout).


L3 adds persistent memory and multi-agent coordination. You implement four memory operations (store, query, compact) and four swarm operations (delegate, discover, report, broadcast).

import { createAgent } from "@clawkernel/sdk";
import type {
MemoryEntry,
MemoryQuery,
SwarmTask,
SwarmContext,
} from "@clawkernel/sdk";
import { randomUUID } from "node:crypto";
// In-memory store (replace with your database in production)
const memoryStore = new Map<
string,
{ id: string; content: string | Record<string, unknown>; timestamp: string }[]
>();
const agent = createAgent({
name: "my-agent",
version: "1.0.0",
// L2 config (tools, policy, sandbox, etc.) ...
tools: { /* ... same as Step 2 ... */ },
policy: { /* ... same as Step 2 ... */ },
sandbox: { check: () => ({ allowed: true }) },
// Memory: persistent state across sessions
memory: {
store: async (storeName: string, entries: MemoryEntry[]) => {
const bucket = memoryStore.get(storeName) ?? [];
const ids: string[] = [];
for (const entry of entries) {
const id = randomUUID();
ids.push(id);
bucket.push({
id,
content: entry.content,
timestamp: new Date().toISOString(),
});
}
memoryStore.set(storeName, bucket);
return { stored: entries.length, ids };
},
query: async (storeName: string, _query: MemoryQuery) => {
const bucket = memoryStore.get(storeName) ?? [];
return {
entries: bucket.map((e) => ({
id: e.id,
content: e.content,
score: 1.0,
timestamp: e.timestamp,
})),
};
},
compact: async (storeName: string) => {
const bucket = memoryStore.get(storeName) ?? [];
const before = bucket.length;
const compacted = bucket.slice(-100);
memoryStore.set(storeName, compacted);
return { entries_before: before, entries_after: compacted.length };
},
},
// Swarm: multi-agent coordination
swarm: {
delegate: async (
_taskId: string,
_task: SwarmTask,
_context: SwarmContext
) => {
return { acknowledged: true };
},
discover: async (_swarmName?: string) => {
return {
peers: [
{
identity: "peer-1",
uri: "claw://local/identity/peer-1",
status: "ready" as const,
},
],
};
},
report: async (
_taskId: string,
_status: string,
_result: Record<string, unknown>
) => {
return { acknowledged: true };
},
broadcast: (_swarmName: string, _message: Record<string, unknown>) => {
// Fire-and-forget notification, no response needed
},
},
});
agent.listen();

The L3 manifest includes all 9 core primitives (Telemetry optional at all levels):

claw: "0.3.0"
kind: Claw
metadata:
name: my-agent
spec:
identity:
inline:
personality: "L3 agent with memory and swarm."
autonomy: autonomous
providers:
- inline:
protocol: openai-compatible
endpoint: http://localhost:11434/v1
model: test-model
auth:
type: none
channels:
- inline:
type: cli
transport: stdio
auth:
secret_ref: CLI_TOKEN
tools:
- inline:
name: echo
description: "Echo input back"
input_schema:
type: object
properties:
text:
type: string
skills:
- inline:
name: echo-skill
description: "Echo workflow"
tools_required:
- echo
instruction: "Use echo tool to echo back user input."
memory:
inline:
stores:
- name: context
type: key-value
backend: sqlite
scope: global
sandbox:
inline:
level: process
policies:
- inline:
rules:
- id: allow-all
action: allow
scope: all
swarm:
inline:
topology: peer-to-peer
agents:
- identity_ref: peer-1
role: peer
coordination:
message_passing: direct
backend: in-process
concurrency:
max_parallel: 2
aggregation:
strategy: merge

Memory — Three operations map to JSON-RPC methods: claw.memory.store persists entries, claw.memory.query retrieves them (with optional semantic search), and claw.memory.compact runs garbage collection.

Swarm — Four operations: claw.swarm.delegate assigns tasks to peers, claw.swarm.discover finds available agents, claw.swarm.report returns task results, and claw.swarm.broadcast sends fire-and-forget notifications to all peers.


Optional Step 4: Add a WorldModel manifest

Section titled “Optional Step 4: Add a WorldModel manifest”

CKP 0.3.0 introduces an optional WorldModel primitive for predictive planning. The SDK does not add new JSON-RPC methods for this; instead, Skills can reference a reusable world model declaration inside the manifest.

claw: "0.3.0"
kind: Claw
metadata:
name: my-agent
spec:
memory:
inline:
stores:
- name: planner-checkpoints
type: checkpoint
role: working
backend: sqlite
world_models:
- inline:
name: route-simulator
paradigm: hybrid
backend:
type: tool
ref: scenario-simulator
planning:
horizon: adaptive
uncertainty_mode: bounded
fallback: conservative
skills:
- inline:
name: route-planning
description: "Plan before acting."
tools_required: ["scenario-simulator"]
instruction: "Simulate outcomes before selecting a route."
world_model_ref: route-simulator

This keeps predictive planning declarative and reusable. Multiple skills can reference the same WorldModel without burying planning logic directly in prompt text.