feat: add framework template adapters

This commit is contained in:
Nyk 2026-03-21 22:21:18 +07:00
parent b8c121ebea
commit dd7d663a36
5 changed files with 1233 additions and 0 deletions

View File

@ -0,0 +1,54 @@
import { NextRequest, NextResponse } from 'next/server'
import { requireRole } from '@/lib/auth'
import { listAdapters } from '@/lib/adapters'
import {
listFrameworks,
getFrameworkInfo,
getTemplatesForFramework,
UNIVERSAL_TEMPLATES,
} from '@/lib/framework-templates'
/**
* GET /api/frameworks List all supported frameworks with connection info and templates.
*
* Query params:
* ?framework=langgraph Get details for a specific framework
* ?templates=true Include available templates in response
*/
export async function GET(request: NextRequest) {
const auth = requireRole(request, 'viewer')
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status })
const { searchParams: n } = new URL(request.url)
const frameworkFilter = n.get('framework')
const includeTemplates = n.get('templates') === 'true'
// Single framework detail
if (frameworkFilter) {
const info = getFrameworkInfo(frameworkFilter)
if (!info) {
return NextResponse.json(
{ error: `Unknown framework: ${frameworkFilter}. Available: ${listAdapters().join(', ')}` },
{ status: 404 }
)
}
const response: Record<string, unknown> = { framework: info }
if (includeTemplates) {
response.templates = getTemplatesForFramework(frameworkFilter)
}
return NextResponse.json(response)
}
// List all frameworks
const frameworks = listFrameworks()
const response: Record<string, unknown> = { frameworks }
if (includeTemplates) {
response.templates = UNIVERSAL_TEMPLATES
}
return NextResponse.json(response)
}
export const dynamic = 'force-dynamic'

View File

@ -0,0 +1,146 @@
/**
* Framework Templates Test Suite
*
* Tests the framework-agnostic template registry, ensuring:
* - All adapters have corresponding framework info
* - Universal templates map correctly to framework-specific configs
* - Template resolution works for all framework/template combinations
*/
import { describe, it, expect } from 'vitest'
import {
FRAMEWORK_REGISTRY,
UNIVERSAL_TEMPLATES,
listFrameworks,
getFrameworkInfo,
getTemplatesForFramework,
getUniversalTemplate,
resolveTemplateConfig,
} from '../framework-templates'
import { listAdapters } from '../adapters'
import { AGENT_TEMPLATES } from '../agent-templates'
describe('Framework Registry', () => {
it('has an entry for every registered adapter', () => {
const adapters = listAdapters()
for (const adapter of adapters) {
expect(FRAMEWORK_REGISTRY[adapter]).toBeDefined()
expect(FRAMEWORK_REGISTRY[adapter].id).toBe(adapter)
}
})
it('every framework has required connection config', () => {
for (const fw of listFrameworks()) {
expect(fw.connection).toBeDefined()
expect(fw.connection.connectionMode).toMatch(/^(webhook|polling|websocket)$/)
expect(fw.connection.heartbeatInterval).toBeGreaterThan(0)
expect(fw.connection.setupHints.length).toBeGreaterThan(0)
expect(fw.connection.exampleSnippet.length).toBeGreaterThan(0)
}
})
it('every framework has a label and description', () => {
for (const fw of listFrameworks()) {
expect(fw.label).toBeTruthy()
expect(fw.description).toBeTruthy()
}
})
it('getFrameworkInfo returns correct framework', () => {
const info = getFrameworkInfo('langgraph')
expect(info?.id).toBe('langgraph')
expect(info?.label).toBe('LangGraph')
})
it('getFrameworkInfo returns undefined for unknown', () => {
expect(getFrameworkInfo('nonexistent')).toBeUndefined()
})
})
describe('Universal Templates', () => {
it('has at least 5 template archetypes', () => {
expect(UNIVERSAL_TEMPLATES.length).toBeGreaterThanOrEqual(5)
})
it('every template has required fields', () => {
for (const tpl of UNIVERSAL_TEMPLATES) {
expect(tpl.type).toBeTruthy()
expect(tpl.label).toBeTruthy()
expect(tpl.description).toBeTruthy()
expect(tpl.emoji).toBeTruthy()
expect(tpl.frameworks.length).toBeGreaterThan(0)
expect(tpl.capabilities.length).toBeGreaterThan(0)
}
})
it('every template supports at least "generic" framework', () => {
for (const tpl of UNIVERSAL_TEMPLATES) {
expect(tpl.frameworks).toContain('generic')
}
})
it('templates with openclawTemplateType reference valid OpenClaw templates', () => {
for (const tpl of UNIVERSAL_TEMPLATES) {
if (tpl.openclawTemplateType) {
const ocTemplate = AGENT_TEMPLATES.find(t => t.type === tpl.openclawTemplateType)
expect(ocTemplate).toBeDefined()
}
}
})
it('getUniversalTemplate returns correct template', () => {
const tpl = getUniversalTemplate('developer')
expect(tpl?.type).toBe('developer')
expect(tpl?.label).toBe('Developer')
})
it('getUniversalTemplate returns undefined for unknown', () => {
expect(getUniversalTemplate('nonexistent')).toBeUndefined()
})
})
describe('Template-Framework Resolution', () => {
it('getTemplatesForFramework returns templates for known frameworks', () => {
for (const fw of listAdapters()) {
const templates = getTemplatesForFramework(fw)
expect(templates.length).toBeGreaterThan(0)
}
})
it('getTemplatesForFramework returns empty for unknown framework', () => {
expect(getTemplatesForFramework('nonexistent')).toEqual([])
})
it('resolveTemplateConfig returns OpenClaw template for openclaw framework', () => {
const result = resolveTemplateConfig('developer', 'openclaw')
expect(result).toBeDefined()
expect(result?.template).toBeDefined()
expect(result?.template?.type).toBe('developer')
expect(result?.universal.type).toBe('developer')
})
it('resolveTemplateConfig returns universal-only for non-openclaw frameworks', () => {
const result = resolveTemplateConfig('developer', 'langgraph')
expect(result).toBeDefined()
expect(result?.template).toBeUndefined()
expect(result?.universal.type).toBe('developer')
})
it('resolveTemplateConfig returns undefined for unknown template', () => {
expect(resolveTemplateConfig('nonexistent', 'generic')).toBeUndefined()
})
it('resolveTemplateConfig returns undefined for unsupported framework', () => {
expect(resolveTemplateConfig('developer', 'nonexistent')).toBeUndefined()
})
it('all universal templates resolve for all their declared frameworks', () => {
for (const tpl of UNIVERSAL_TEMPLATES) {
for (const fw of tpl.frameworks) {
const result = resolveTemplateConfig(tpl.type, fw)
expect(result).toBeDefined()
expect(result?.universal.type).toBe(tpl.type)
}
}
})
})

View File

@ -0,0 +1,200 @@
/**
* Adapter API Route Integration Tests
*
* Tests the POST /api/adapters dispatcher against all frameworks.
* Simulates what an external agent would do to connect to Mission Control.
*
* This is the "Feynman test" timing how long it takes a stranger's
* agent to connect via the HTTP API.
*/
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { getAdapter, listAdapters } from '../index'
// These tests verify the API contract from the external agent's perspective.
// They don't hit the HTTP layer (that's E2E) but verify the adapter dispatch
// logic matches what the API route does.
const mockBroadcast = vi.fn()
vi.mock('@/lib/event-bus', () => ({
eventBus: { broadcast: (...args: unknown[]) => mockBroadcast(...args) },
}))
const mockQuery = vi.fn()
vi.mock('../adapter', async (importOriginal) => {
const original = await importOriginal<typeof import('../adapter')>()
return {
...original,
queryPendingAssignments: (...args: unknown[]) => mockQuery(...args),
}
})
// Simulate what POST /api/adapters does internally
async function simulateAdapterAction(
framework: string,
action: string,
payload: Record<string, unknown>
): Promise<{ ok?: boolean; assignments?: unknown[]; error?: string }> {
let adapter
try {
adapter = getAdapter(framework)
} catch {
return { error: `Unknown framework: ${framework}` }
}
switch (action) {
case 'register': {
const { agentId, name, metadata } = payload
if (!agentId || !name) return { error: 'payload.agentId and payload.name required' }
await adapter.register({
agentId: agentId as string,
name: name as string,
framework,
metadata: metadata as Record<string, unknown>,
})
return { ok: true }
}
case 'heartbeat': {
const { agentId, status, metrics } = payload
if (!agentId) return { error: 'payload.agentId required' }
await adapter.heartbeat({
agentId: agentId as string,
status: (status as string) || 'online',
metrics: metrics as Record<string, unknown>,
})
return { ok: true }
}
case 'report': {
const { taskId, agentId, progress, status, output } = payload
if (!taskId || !agentId) return { error: 'payload.taskId and payload.agentId required' }
await adapter.reportTask({
taskId: taskId as string,
agentId: agentId as string,
progress: (progress as number) ?? 0,
status: (status as string) || 'in_progress',
output,
})
return { ok: true }
}
case 'assignments': {
const { agentId } = payload
if (!agentId) return { error: 'payload.agentId required' }
const assignments = await adapter.getAssignments(agentId as string)
return { assignments }
}
case 'disconnect': {
const { agentId } = payload
if (!agentId) return { error: 'payload.agentId required' }
await adapter.disconnect(agentId as string)
return { ok: true }
}
default:
return { error: `Unknown action: ${action}` }
}
}
describe('Adapter API dispatch', () => {
beforeEach(() => {
mockBroadcast.mockClear()
mockQuery.mockClear()
})
// Full lifecycle for every framework
describe.each(listAdapters())('Full agent lifecycle: %s', (framework) => {
it('register → heartbeat → report → assignments → disconnect', async () => {
mockQuery.mockResolvedValue([{ taskId: '1', description: 'Do stuff', priority: 1 }])
// 1. Register
const reg = await simulateAdapterAction(framework, 'register', {
agentId: `${framework}-agent-1`,
name: `${framework} Test Agent`,
metadata: { version: '2.0' },
})
expect(reg.ok).toBe(true)
// 2. Heartbeat
const hb = await simulateAdapterAction(framework, 'heartbeat', {
agentId: `${framework}-agent-1`,
status: 'busy',
metrics: { tasksInProgress: 1 },
})
expect(hb.ok).toBe(true)
// 3. Report task progress
const rpt = await simulateAdapterAction(framework, 'report', {
taskId: 'task-abc',
agentId: `${framework}-agent-1`,
progress: 50,
status: 'in_progress',
output: { log: 'halfway done' },
})
expect(rpt.ok).toBe(true)
// 4. Get assignments
const asgn = await simulateAdapterAction(framework, 'assignments', {
agentId: `${framework}-agent-1`,
})
expect(asgn.assignments).toHaveLength(1)
// 5. Disconnect
const disc = await simulateAdapterAction(framework, 'disconnect', {
agentId: `${framework}-agent-1`,
})
expect(disc.ok).toBe(true)
// Verify event sequence
const eventTypes = mockBroadcast.mock.calls.map(c => c[0])
expect(eventTypes).toEqual([
'agent.created',
'agent.status_changed',
'task.updated',
'agent.status_changed',
])
})
})
// Validation checks
describe('input validation', () => {
it('rejects unknown framework', async () => {
const result = await simulateAdapterAction('totally-fake', 'register', {
agentId: 'x', name: 'X',
})
expect(result.error).toContain('Unknown framework')
})
it('rejects unknown action', async () => {
const result = await simulateAdapterAction('generic', 'explode', {})
expect(result.error).toContain('Unknown action')
})
it('rejects register without agentId', async () => {
const result = await simulateAdapterAction('generic', 'register', { name: 'No ID' })
expect(result.error).toContain('agentId')
})
it('rejects register without name', async () => {
const result = await simulateAdapterAction('generic', 'register', { agentId: 'no-name' })
expect(result.error).toContain('name')
})
it('rejects heartbeat without agentId', async () => {
const result = await simulateAdapterAction('generic', 'heartbeat', {})
expect(result.error).toContain('agentId')
})
it('rejects report without taskId', async () => {
const result = await simulateAdapterAction('generic', 'report', { agentId: 'x' })
expect(result.error).toContain('taskId')
})
it('rejects assignments without agentId', async () => {
const result = await simulateAdapterAction('generic', 'assignments', {})
expect(result.error).toContain('agentId')
})
it('rejects disconnect without agentId', async () => {
const result = await simulateAdapterAction('generic', 'disconnect', {})
expect(result.error).toContain('agentId')
})
})
})

View File

@ -0,0 +1,406 @@
/**
* Adapter Compliance Test Suite
*
* Tests every FrameworkAdapter implementation against the contract.
* This is the P0 gate nothing ships until all adapters pass.
*
* Tests:
* 1. Interface compliance (all 5 methods exist and are callable)
* 2. Event emission (correct event types and payloads)
* 3. Assignment retrieval (DB query works)
* 4. Error resilience (bad inputs don't crash)
* 5. Framework identity (each adapter tags events correctly)
*/
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import type { FrameworkAdapter, AgentRegistration, HeartbeatPayload, TaskReport } from '../adapter'
import { getAdapter, listAdapters } from '../index'
// Mock event bus
const mockBroadcast = vi.fn()
vi.mock('@/lib/event-bus', () => ({
eventBus: { broadcast: (...args: unknown[]) => mockBroadcast(...args) },
}))
// Mock DB query for getAssignments
const mockQuery = vi.fn()
vi.mock('../adapter', async (importOriginal) => {
const original = await importOriginal<typeof import('../adapter')>()
return {
...original,
queryPendingAssignments: (...args: unknown[]) => mockQuery(...args),
}
})
// ─── Test Data ───────────────────────────────────────────────────────────────
const testAgent: AgentRegistration = {
agentId: 'test-agent-001',
name: 'Test Agent',
framework: 'test-framework',
metadata: { version: '1.0', runtime: 'node' },
}
const testHeartbeat: HeartbeatPayload = {
agentId: 'test-agent-001',
status: 'busy',
metrics: { cpu: 42, memory: 1024, tasksCompleted: 5 },
}
const testReport: TaskReport = {
taskId: 'task-123',
agentId: 'test-agent-001',
progress: 75,
status: 'in_progress',
output: { summary: 'Processing step 3 of 4' },
}
// ─── Shared Compliance Tests ─────────────────────────────────────────────────
const ALL_FRAMEWORKS = ['openclaw', 'generic', 'crewai', 'langgraph', 'autogen', 'claude-sdk']
describe('Adapter Registry', () => {
it('lists all registered adapters', () => {
const adapters = listAdapters()
expect(adapters).toEqual(expect.arrayContaining(ALL_FRAMEWORKS))
expect(adapters.length).toBe(ALL_FRAMEWORKS.length)
})
it('returns an adapter for each registered framework', () => {
for (const fw of ALL_FRAMEWORKS) {
const adapter = getAdapter(fw)
expect(adapter).toBeDefined()
expect(adapter.framework).toBe(fw)
}
})
it('throws for unknown framework', () => {
expect(() => getAdapter('nonexistent')).toThrow('Unknown framework adapter')
})
})
// Run the full compliance suite for EVERY adapter
describe.each(ALL_FRAMEWORKS)('FrameworkAdapter compliance: %s', (framework) => {
let adapter: FrameworkAdapter
beforeEach(() => {
adapter = getAdapter(framework)
mockBroadcast.mockClear()
mockQuery.mockClear()
})
// ── 1. Interface Compliance ──────────────────────────────────────────────
describe('interface compliance', () => {
it('implements all 5 required methods', () => {
expect(typeof adapter.register).toBe('function')
expect(typeof adapter.heartbeat).toBe('function')
expect(typeof adapter.reportTask).toBe('function')
expect(typeof adapter.getAssignments).toBe('function')
expect(typeof adapter.disconnect).toBe('function')
})
it('has a readonly framework property', () => {
expect(adapter.framework).toBe(framework)
})
it('all methods return promises', async () => {
mockQuery.mockResolvedValue([])
const results = [
adapter.register(testAgent),
adapter.heartbeat(testHeartbeat),
adapter.reportTask(testReport),
adapter.getAssignments('any-id'),
adapter.disconnect('any-id'),
]
// All should be thenables
for (const r of results) {
expect(r).toBeInstanceOf(Promise)
}
await Promise.all(results)
})
})
// ── 2. Event Emission ────────────────────────────────────────────────────
describe('register()', () => {
it('broadcasts agent.created with correct payload', async () => {
await adapter.register(testAgent)
expect(mockBroadcast).toHaveBeenCalledTimes(1)
expect(mockBroadcast).toHaveBeenCalledWith(
'agent.created',
expect.objectContaining({
id: 'test-agent-001',
name: 'Test Agent',
status: 'online',
})
)
})
it('includes framework tag in event', async () => {
await adapter.register(testAgent)
const payload = mockBroadcast.mock.calls[0][1]
// Generic adapter may use agent.framework; others use this.framework
expect(payload.framework).toBeTruthy()
})
it('passes through metadata', async () => {
await adapter.register(testAgent)
const payload = mockBroadcast.mock.calls[0][1]
// Metadata is spread into the event payload
expect(payload.version).toBe('1.0')
expect(payload.runtime).toBe('node')
})
it('handles agent with no metadata', async () => {
await adapter.register({
agentId: 'minimal-agent',
name: 'Minimal',
framework,
})
expect(mockBroadcast).toHaveBeenCalledWith(
'agent.created',
expect.objectContaining({
id: 'minimal-agent',
name: 'Minimal',
status: 'online',
})
)
})
})
describe('heartbeat()', () => {
it('broadcasts agent.status_changed with status and metrics', async () => {
await adapter.heartbeat(testHeartbeat)
expect(mockBroadcast).toHaveBeenCalledTimes(1)
expect(mockBroadcast).toHaveBeenCalledWith(
'agent.status_changed',
expect.objectContaining({
id: 'test-agent-001',
status: 'busy',
})
)
})
it('includes metrics in event payload', async () => {
await adapter.heartbeat(testHeartbeat)
const payload = mockBroadcast.mock.calls[0][1]
expect(payload.metrics).toBeDefined()
expect(payload.metrics.cpu).toBe(42)
})
it('handles heartbeat with no metrics', async () => {
await adapter.heartbeat({
agentId: 'test-agent-001',
status: 'idle',
})
expect(mockBroadcast).toHaveBeenCalledWith(
'agent.status_changed',
expect.objectContaining({
id: 'test-agent-001',
status: 'idle',
})
)
})
})
describe('reportTask()', () => {
it('broadcasts task.updated with progress and status', async () => {
await adapter.reportTask(testReport)
expect(mockBroadcast).toHaveBeenCalledTimes(1)
expect(mockBroadcast).toHaveBeenCalledWith(
'task.updated',
expect.objectContaining({
id: 'task-123',
agentId: 'test-agent-001',
progress: 75,
status: 'in_progress',
})
)
})
it('passes through output data', async () => {
await adapter.reportTask(testReport)
const payload = mockBroadcast.mock.calls[0][1]
expect(payload.output).toEqual({ summary: 'Processing step 3 of 4' })
})
it('handles report with no output', async () => {
await adapter.reportTask({
taskId: 'task-456',
agentId: 'test-agent-001',
progress: 100,
status: 'completed',
})
expect(mockBroadcast).toHaveBeenCalledWith(
'task.updated',
expect.objectContaining({
id: 'task-456',
status: 'completed',
progress: 100,
})
)
})
})
describe('getAssignments()', () => {
it('delegates to queryPendingAssignments', async () => {
const mockAssignments = [
{ taskId: '1', description: 'Fix bug', priority: 1 },
{ taskId: '2', description: 'Write tests', priority: 2 },
]
mockQuery.mockResolvedValue(mockAssignments)
const result = await adapter.getAssignments('test-agent-001')
expect(mockQuery).toHaveBeenCalledWith('test-agent-001')
expect(result).toEqual(mockAssignments)
})
it('returns empty array when no assignments', async () => {
mockQuery.mockResolvedValue([])
const result = await adapter.getAssignments('idle-agent')
expect(result).toEqual([])
})
it('does not broadcast events', async () => {
mockQuery.mockResolvedValue([])
await adapter.getAssignments('test-agent-001')
expect(mockBroadcast).not.toHaveBeenCalled()
})
})
describe('disconnect()', () => {
it('broadcasts agent.status_changed with offline status', async () => {
await adapter.disconnect('test-agent-001')
expect(mockBroadcast).toHaveBeenCalledTimes(1)
expect(mockBroadcast).toHaveBeenCalledWith(
'agent.status_changed',
expect.objectContaining({
id: 'test-agent-001',
status: 'offline',
})
)
})
it('tags disconnect event with framework', async () => {
await adapter.disconnect('test-agent-001')
const payload = mockBroadcast.mock.calls[0][1]
expect(payload.framework).toBe(framework)
})
})
// ── 3. Framework Identity ────────────────────────────────────────────────
describe('framework identity', () => {
it('tags all emitted events with its framework name', async () => {
mockQuery.mockResolvedValue([])
await adapter.register(testAgent)
await adapter.heartbeat(testHeartbeat)
await adapter.reportTask(testReport)
await adapter.disconnect('test-agent-001')
// All 4 event-emitting calls should tag with framework
for (const call of mockBroadcast.mock.calls) {
const payload = call[1]
expect(payload.framework).toBeTruthy()
}
})
})
})
// ── 4. Cross-Adapter Behavioral Consistency ────────────────────────────────
describe('Cross-adapter consistency', () => {
beforeEach(() => {
mockBroadcast.mockClear()
mockQuery.mockClear()
})
it('all adapters emit the same event types for the same actions', async () => {
const eventsByFramework: Record<string, string[]> = {}
for (const fw of ALL_FRAMEWORKS) {
mockBroadcast.mockClear()
mockQuery.mockResolvedValue([])
const adapter = getAdapter(fw)
await adapter.register(testAgent)
await adapter.heartbeat(testHeartbeat)
await adapter.reportTask(testReport)
await adapter.disconnect('test-agent-001')
eventsByFramework[fw] = mockBroadcast.mock.calls.map(c => c[0])
}
const expected = ['agent.created', 'agent.status_changed', 'task.updated', 'agent.status_changed']
for (const fw of ALL_FRAMEWORKS) {
expect(eventsByFramework[fw]).toEqual(expected)
}
})
it('all adapters return the same assignment data for the same agent', async () => {
const mockAssignments = [{ taskId: '99', description: 'Shared task', priority: 0 }]
mockQuery.mockResolvedValue(mockAssignments)
for (const fw of ALL_FRAMEWORKS) {
const adapter = getAdapter(fw)
const result = await adapter.getAssignments('shared-agent')
expect(result).toEqual(mockAssignments)
}
})
})
// ── 5. Generic Adapter Specialization ──────────────────────────────────────
describe('GenericAdapter special behavior', () => {
beforeEach(() => {
mockBroadcast.mockClear()
})
it('respects agent.framework from registration payload', async () => {
const adapter = getAdapter('generic')
await adapter.register({
agentId: 'custom-agent',
name: 'Custom Framework Agent',
framework: 'my-custom-framework',
})
const payload = mockBroadcast.mock.calls[0][1]
expect(payload.framework).toBe('my-custom-framework')
})
it('falls back to "generic" when no framework in payload', async () => {
const adapter = getAdapter('generic')
await adapter.register({
agentId: 'unknown-agent',
name: 'Unknown Agent',
framework: '',
})
const payload = mockBroadcast.mock.calls[0][1]
// Empty string is falsy, should fall back to 'generic'
expect(payload.framework).toBe('generic')
})
})

View File

@ -0,0 +1,427 @@
/**
* Framework-Agnostic Template System
*
* Extends the existing OpenClaw templates with framework-neutral archetypes
* that any adapter can use. Each framework template defines:
* - What the agent does (role, capabilities)
* - How it connects (framework-specific connection config)
* - What permissions it needs (tool scopes)
*
* The existing AGENT_TEMPLATES in agent-templates.ts remain for OpenClaw-native
* use. This module wraps them with a framework-aware registry.
*/
import { AGENT_TEMPLATES, type AgentTemplate } from './agent-templates'
import { listAdapters } from './adapters'
// ─── Framework Connection Config ────────────────────────────────────────────
export interface FrameworkConnectionConfig {
/** How the agent connects to MC (webhook, polling, websocket) */
connectionMode: 'webhook' | 'polling' | 'websocket'
/** Default heartbeat interval in seconds */
heartbeatInterval: number
/** Framework-specific setup hints shown in the UI */
setupHints: string[]
/** Example connection code snippet */
exampleSnippet: string
}
export interface FrameworkInfo {
id: string
label: string
description: string
docsUrl: string
connection: FrameworkConnectionConfig
}
// ─── Framework Registry ─────────────────────────────────────────────────────
export const FRAMEWORK_REGISTRY: Record<string, FrameworkInfo> = {
openclaw: {
id: 'openclaw',
label: 'OpenClaw',
description: 'Native gateway-managed agents with full lifecycle control',
docsUrl: 'https://github.com/openclaw/openclaw',
connection: {
connectionMode: 'websocket',
heartbeatInterval: 30,
setupHints: [
'Agents are managed via the OpenClaw gateway',
'Config syncs bidirectionally via openclaw.json',
'Use "pnpm openclaw agents add" to provision',
],
exampleSnippet: `# OpenClaw agents are auto-managed by the gateway.
# No manual registration needed sync happens automatically.
# See: openclaw.json in your state directory.`,
},
},
generic: {
id: 'generic',
label: 'Generic HTTP',
description: 'Any agent that can make HTTP calls — the universal adapter',
docsUrl: '',
connection: {
connectionMode: 'polling',
heartbeatInterval: 60,
setupHints: [
'POST to /api/adapters with framework: "generic"',
'Use any language — just call the REST API',
'Poll /api/adapters for assignments or use SSE for push',
],
exampleSnippet: `# Register your agent
curl -X POST http://localhost:3000/api/adapters \\
-H "Content-Type: application/json" \\
-H "x-api-key: YOUR_API_KEY" \\
-d '{
"framework": "generic",
"action": "register",
"payload": {
"agentId": "my-agent-1",
"name": "My Custom Agent",
"metadata": { "version": "1.0" }
}
}'
# Send heartbeat
curl -X POST http://localhost:3000/api/adapters \\
-H "Content-Type: application/json" \\
-H "x-api-key: YOUR_API_KEY" \\
-d '{
"framework": "generic",
"action": "heartbeat",
"payload": { "agentId": "my-agent-1", "status": "online" }
}'
# Get assignments
curl -X POST http://localhost:3000/api/adapters \\
-H "Content-Type: application/json" \\
-H "x-api-key: YOUR_API_KEY" \\
-d '{
"framework": "generic",
"action": "assignments",
"payload": { "agentId": "my-agent-1" }
}'`,
},
},
langgraph: {
id: 'langgraph',
label: 'LangGraph',
description: 'LangChain\'s graph-based agent orchestration framework',
docsUrl: 'https://langchain-ai.github.io/langgraph/',
connection: {
connectionMode: 'webhook',
heartbeatInterval: 30,
setupHints: [
'Wrap your LangGraph graph with the MC adapter client',
'Register nodes as capabilities for task routing',
'Use checkpointers for durable state across MC task assignments',
],
exampleSnippet: `import requests
MC_URL = "http://localhost:3000"
API_KEY = "YOUR_API_KEY"
HEADERS = {"Content-Type": "application/json", "x-api-key": API_KEY}
# Register your LangGraph agent
requests.post(f"{MC_URL}/api/adapters", headers=HEADERS, json={
"framework": "langgraph",
"action": "register",
"payload": {
"agentId": "langgraph-research-agent",
"name": "Research Agent",
"metadata": {
"graph_type": "StateGraph",
"nodes": ["research", "summarize", "review"],
"checkpointer": "sqlite"
}
}
})
# After your graph completes a task:
requests.post(f"{MC_URL}/api/adapters", headers=HEADERS, json={
"framework": "langgraph",
"action": "report",
"payload": {
"taskId": "task-123",
"agentId": "langgraph-research-agent",
"progress": 100,
"status": "completed",
"output": {"summary": "Research complete", "sources": 12}
}
})`,
},
},
crewai: {
id: 'crewai',
label: 'CrewAI',
description: 'Role-based multi-agent orchestration framework',
docsUrl: 'https://docs.crewai.com/',
connection: {
connectionMode: 'webhook',
heartbeatInterval: 30,
setupHints: [
'Register each CrewAI agent role as a separate MC agent',
'Map Crew tasks to MC task assignments',
'Use callbacks to report progress back to MC',
],
exampleSnippet: `from crewai import Agent, Task, Crew
import requests
MC_URL = "http://localhost:3000"
HEADERS = {"Content-Type": "application/json", "x-api-key": "YOUR_API_KEY"}
def register_crew_agent(agent: Agent):
"""Register a CrewAI agent with Mission Control."""
requests.post(f"{MC_URL}/api/adapters", headers=HEADERS, json={
"framework": "crewai",
"action": "register",
"payload": {
"agentId": f"crewai-{agent.role.lower().replace(' ', '-')}",
"name": agent.role,
"metadata": {
"goal": agent.goal,
"backstory": agent.backstory[:200],
"tools": [t.name for t in (agent.tools or [])]
}
}
})
def report_task_complete(agent_id: str, task_id: str, output: str):
"""Report task completion to Mission Control."""
requests.post(f"{MC_URL}/api/adapters", headers=HEADERS, json={
"framework": "crewai",
"action": "report",
"payload": {
"taskId": task_id,
"agentId": agent_id,
"progress": 100,
"status": "completed",
"output": {"result": output}
}
})`,
},
},
autogen: {
id: 'autogen',
label: 'AutoGen',
description: 'Microsoft\'s multi-agent conversation framework',
docsUrl: 'https://microsoft.github.io/autogen/',
connection: {
connectionMode: 'webhook',
heartbeatInterval: 30,
setupHints: [
'Register each AutoGen AssistantAgent with MC',
'Use message hooks to report conversation progress',
'Map GroupChat rounds to MC task progress updates',
],
exampleSnippet: `import requests
# AutoGen v0.4+ (ag2)
from autogen import AssistantAgent, UserProxyAgent
MC_URL = "http://localhost:3000"
HEADERS = {"Content-Type": "application/json", "x-api-key": "YOUR_API_KEY"}
def register_autogen_agent(agent_name: str, system_message: str):
"""Register an AutoGen agent with Mission Control."""
requests.post(f"{MC_URL}/api/adapters", headers=HEADERS, json={
"framework": "autogen",
"action": "register",
"payload": {
"agentId": f"autogen-{agent_name.lower().replace(' ', '-')}",
"name": agent_name,
"metadata": {
"type": "AssistantAgent",
"system_message_preview": system_message[:200]
}
}
})
# Register your agents
register_autogen_agent("Coder", "You are a coding assistant...")
register_autogen_agent("Reviewer", "You review code for bugs...")`,
},
},
'claude-sdk': {
id: 'claude-sdk',
label: 'Claude Agent SDK',
description: 'Anthropic\'s native agent SDK for building Claude-powered agents',
docsUrl: 'https://docs.anthropic.com/en/docs/agents/agent-sdk',
connection: {
connectionMode: 'webhook',
heartbeatInterval: 30,
setupHints: [
'Register your Claude Agent SDK agent after initialization',
'Use tool callbacks to report progress to MC',
'Map agent turns to MC task progress updates',
],
exampleSnippet: `import Anthropic from "@anthropic-ai/sdk";
const MC_URL = "http://localhost:3000";
const HEADERS = { "Content-Type": "application/json", "x-api-key": "YOUR_API_KEY" };
// Register your Claude SDK agent
await fetch(\`\${MC_URL}/api/adapters\`, {
method: "POST",
headers: HEADERS,
body: JSON.stringify({
framework: "claude-sdk",
action: "register",
payload: {
agentId: "claude-agent-1",
name: "Claude Development Agent",
metadata: {
model: "claude-sonnet-4-20250514",
tools: ["computer", "text_editor", "bash"]
}
}
})
});
// Report task completion
await fetch(\`\${MC_URL}/api/adapters\`, {
method: "POST",
headers: HEADERS,
body: JSON.stringify({
framework: "claude-sdk",
action: "report",
payload: {
taskId: "task-456",
agentId: "claude-agent-1",
progress: 100,
status: "completed",
output: { files_changed: 3, tests_passed: true }
}
})
});`,
},
},
}
// ─── Universal Template Archetypes ──────────────────────────────────────────
export interface UniversalTemplate {
type: string
label: string
description: string
emoji: string
/** Which frameworks this template supports */
frameworks: string[]
/** Role-based capabilities (framework-agnostic) */
capabilities: string[]
/** The OpenClaw template to use when framework is openclaw */
openclawTemplateType?: string
}
/**
* Universal templates that work across all frameworks.
* These describe WHAT the agent does, not HOW it's configured.
* Framework-specific config is resolved at creation time.
*/
export const UNIVERSAL_TEMPLATES: UniversalTemplate[] = [
{
type: 'orchestrator',
label: 'Orchestrator',
description: 'Coordinates other agents, routes tasks, and manages workflows. Full access.',
emoji: '\ud83e\udded',
frameworks: ['openclaw', 'generic', 'langgraph', 'crewai', 'autogen', 'claude-sdk'],
capabilities: ['task_routing', 'agent_management', 'workflow_control', 'full_access'],
openclawTemplateType: 'orchestrator',
},
{
type: 'developer',
label: 'Developer',
description: 'Writes and edits code, runs builds and tests. Read-write workspace access.',
emoji: '\ud83d\udee0\ufe0f',
frameworks: ['openclaw', 'generic', 'langgraph', 'crewai', 'autogen', 'claude-sdk'],
capabilities: ['code_write', 'code_execute', 'testing', 'debugging'],
openclawTemplateType: 'developer',
},
{
type: 'reviewer',
label: 'Reviewer / QA',
description: 'Reviews code and validates quality. Read-only access, lightweight model.',
emoji: '\ud83d\udd2c',
frameworks: ['openclaw', 'generic', 'langgraph', 'crewai', 'autogen', 'claude-sdk'],
capabilities: ['code_read', 'quality_review', 'security_audit'],
openclawTemplateType: 'reviewer',
},
{
type: 'researcher',
label: 'Researcher',
description: 'Browses the web and gathers information. No code execution.',
emoji: '\ud83d\udd0d',
frameworks: ['openclaw', 'generic', 'langgraph', 'crewai', 'autogen', 'claude-sdk'],
capabilities: ['web_browse', 'data_gathering', 'summarization'],
openclawTemplateType: 'researcher',
},
{
type: 'content-creator',
label: 'Content Creator',
description: 'Generates and edits written content. No code execution or browsing.',
emoji: '\u270f\ufe0f',
frameworks: ['openclaw', 'generic', 'langgraph', 'crewai', 'autogen', 'claude-sdk'],
capabilities: ['content_write', 'content_edit'],
openclawTemplateType: 'content-creator',
},
{
type: 'security-auditor',
label: 'Security Auditor',
description: 'Scans for vulnerabilities. Read-only with shell access for scanning tools.',
emoji: '\ud83d\udee1\ufe0f',
frameworks: ['openclaw', 'generic', 'langgraph', 'crewai', 'autogen', 'claude-sdk'],
capabilities: ['code_read', 'shell_execute', 'security_scan'],
openclawTemplateType: 'security-auditor',
},
]
// ─── Template Resolution ────────────────────────────────────────────────────
/**
* Get a universal template by type.
*/
export function getUniversalTemplate(type: string): UniversalTemplate | undefined {
return UNIVERSAL_TEMPLATES.find(t => t.type === type)
}
/**
* List templates available for a specific framework.
*/
export function getTemplatesForFramework(framework: string): UniversalTemplate[] {
return UNIVERSAL_TEMPLATES.filter(t => t.frameworks.includes(framework))
}
/**
* Get framework connection info.
*/
export function getFrameworkInfo(framework: string): FrameworkInfo | undefined {
return FRAMEWORK_REGISTRY[framework]
}
/**
* List all supported frameworks.
*/
export function listFrameworks(): FrameworkInfo[] {
return Object.values(FRAMEWORK_REGISTRY)
}
/**
* Resolve a universal template to its OpenClaw-specific config (if applicable).
* For non-OpenClaw frameworks, returns the universal template metadata
* since config is managed externally by the framework.
*/
export function resolveTemplateConfig(
universalType: string,
framework: string
): { template?: AgentTemplate; universal: UniversalTemplate } | undefined {
const universal = getUniversalTemplate(universalType)
if (!universal) return undefined
if (!universal.frameworks.includes(framework)) return undefined
if (framework === 'openclaw' && universal.openclawTemplateType) {
const template = AGENT_TEMPLATES.find(t => t.type === universal.openclawTemplateType)
return { template, universal }
}
return { universal }
}