@compyle/unagent
v1.0.0
Published
Unified AI Agents Framework - A unified AI Agent development framework
Readme
Unagent - Unified AI Agent Development Framework
A unified development framework designed for multiple AI Clients (Claude Agent SDK, Codex, etc.), using a publish-subscribe pattern with support for asynchronous event handling, error isolation, and type safety.
Features
- ✅ Unified Event System - All AI clients use the same event format
- ✅ Async Event Handling - Complete async listener support
- ✅ Error Isolation - Single listener errors don't affect other listeners
- ✅ Type Safety - Complete TypeScript type definitions
- ✅ Extensibility - Plugin system and unified Client interface
- ✅ Agent SDK Integration - Based on the latest Anthropic Claude Agent SDK
Architecture Layers
Application Layer (custom listeners, business logic)
↓
Event Bus (EventEmitter)
↓
Client Abstraction Layer (Claude Agent SDK, Codex)Installation
npm install @compyle/unagentDependencies
This project uses @anthropic-ai/claude-agent-sdk, which requires installing Claude Code CLI first:
# macOS/Linux/WSL
curl -fsSL https://claude.ai/install.sh | bash
# Or use Homebrew
brew install --cask claude-codeTests
npm run testQuick Start
Using Claude Agent SDK
import {
ClaudeClient,
getEventBus,
} from "@compyle/unagent";
// 1. Get the event bus and set up custom listeners
const eventBus = getEventBus();
// Listen to all events
eventBus.on("event", (event) => {
console.log("Event:", event.type);
});
// 2. Use Claude Agent Client
const claude = new ClaudeClient({
apiKey: process.env.ANTHROPIC_API_KEY,
allowedTools: ["Read", "Write", "Edit", "Bash", "Glob", "Grep"],
permissionMode: "bypassPermissions",
});
// Stream call
const { events } = await claude.runStream({
query: "Analyze the current project structure and summarize",
});
for await (const event of events) {
// Handle events
if (event.type === "item.end") {
console.log(event.message);
}
}Define a Custom Tool
import { defineTool } from "@compyle/unagent";
import { z } from "zod";
const repoStats = defineTool({
name: "repo_stats",
description: "Summarize repository size and extensions",
inputSchema: {
path: z.string().min(1),
},
handler: async ({ path }) => `Path: ${path}`,
});Using Custom Agents
const customAgent = new ClaudeClient({
apiKey: process.env.ANTHROPIC_API_KEY,
allowedTools: ["Read", "Grep", "Glob", "Task"],
permissionMode: "bypassPermissions",
// Define custom sub agents
agents: {
"code-reviewer": {
description: "Expert code review agent",
prompt: "Analyze code quality, security, and best practices.",
},
},
});
const result = await customAgent.run({
query: "Review code using code-reviewer agent",
});Read-only Agent (Safe Mode)
const readOnlyAgent = new ClaudeClient({
apiKey: process.env.ANTHROPIC_API_KEY,
allowedTools: ["Read", "Glob", "Grep"], // Read-only operations
permissionMode: "bypassPermissions",
});
const { events } = await readOnlyAgent.runStream({
query: "Analyze project structure",
});Using Codex Client with Permission Modes
import { CodexClient } from "@compyle/unagent";
// Safe mode: read-only access
const readOnlyCodex = new CodexClient({
apiKey: process.env.OPENAI_API_KEY,
permissionMode: "auto", // Maps to "read-only" sandbox
model: "gpt-4o",
});
// Workspace write mode: can modify files in workspace
const writeCodex = new CodexClient({
apiKey: process.env.OPENAI_API_KEY,
permissionMode: "acceptEdits", // Maps to "workspace-write" sandbox
model: "gpt-4o",
addDirs: ["/tmp", "/var/tmp"], // Additional writable directories
});
// Danger mode: full system access (use with caution!)
const fullAccessCodex = new CodexClient({
apiKey: process.env.OPENAI_API_KEY,
permissionMode: "bypassPermissions", // Maps to "danger-full-access" sandbox
model: "gpt-4o",
});
const { events } = await writeCodex.runStream({
query: "Create a new file and update existing code",
});Python Usage
This repository includes a Python bridge package that calls the library through a Node.js process.
Install with uv (Recommended)
# Install uv
curl -LsSf https://astral.sh/uv/install.sh | sh
# Or use Homebrew
brew install uv
# Install project dependencies
cd python
bash install_with_uv.sh
# Activate virtual environment
source .venv/bin/activate
# Run examples
ANTHROPIC_API_KEY=your_key python examples/python-demo/main.pyTraditional Installation Method
npm run build
python -m venv .venv
source .venv/bin/activate
pip install -e ./python
ANTHROPIC_API_KEY=your_key python examples/python-demo/main.pyfrom unagent import ClaudeClient
client = ClaudeClient({"enable_message_mode": True})
stream = client.run_stream({"query": "Summarize the repo"})
for event in stream["events"]:
print(event["type"])Examples
Node.js Demo (Orchestration + Custom Tools + Event Bus)
export ANTHROPIC_API_KEY=your_claude_key
cd examples/nodejs-demo
npm install
npm start "Analyze src/ and generate documentation with repo stats"Python Demos
export ANTHROPIC_API_KEY=your_claude_key
python examples/python-demo/main.py "Analyze src/ and generate documentation with repo stats"
export CODEX_API_KEY=your_codex_key
python examples/python-demo/multi_agent_collaboration.py "Draft an API checklist for a new service"Core Components
1. Event System (src/events/)
Event Types (event-types.ts)
Defines all unified event types:
SessionStartEvent- Session startsSessionEndEvent- Session endsItemStartEvent- Message/item startsItemChunkEvent- Streaming chunk for a message/itemItemEndEvent- Message/item ends with full messageErrorEvent- Error event
Event Bus (event-bus.ts)
Provides an event bus using the publish-subscribe pattern:
import { getEventBus } from "@compyle/unagent";
const eventBus = getEventBus();
// Register listeners
eventBus.on("event", (event) => {
console.log("Event:", event);
});
// Emit events
eventBus.emit({
type: "session.start",
timestamp: Date.now(),
clientType: ClientType.CLAUDE,
sessionId: "session-123",
params: { query: "Hello" },
});2. Client Abstraction Layer (src/client/)
BaseClient
Base class for all AI clients, defining a unified interface:
interface IBaseClient {
run(params: ClientRunParams): Promise<ClientRunResult>;
runStream(params: ClientRunParams): Promise<ClientStreamResult>;
on(listener: EventListener): string;
once(listener: EventListener): string;
off(listenerOrId: string | EventListener): void;
removeAllListeners(): void;
getClientType(): ClientType;
}ClaudeClient
Implementation based on Anthropic Agent SDK:
const claude = new ClaudeClient({
apiKey: process.env.ANTHROPIC_API_KEY,
allowedTools: ["Read", "Write", "Edit", "Bash"],
permissionMode: "bypassPermissions",
useProjectConfig: true, // Read .claude/ directory config
});
// Stream call
const { events } = await claude.runStream({ query: "Analyze code" });
// Non-stream call
const result = await claude.run({ query: "Analyze code" });ClaudeClient Configuration Options:
| Option | Type | Description |
|------|------|------|
| apiKey | string | Anthropic API key |
| allowedTools | string[] | List of allowed tools |
| permissionMode | "auto" \| "bypassPermissions" \| "acceptEdits" | Permission mode |
| agents | Record<string, AgentDefinition> | Custom sub agents |
| mcpServers | Record<string, McpServerConfig> | MCP server configuration |
| hooks | Record<string, any> | Hooks configuration |
| useProjectConfig | boolean | Whether to use project config |
| workingDirectory | string | Working directory |
Using Codex Client
import { CodexClient } from "@compyle/unagent";
const codex = new CodexClient({
apiKey: process.env.OPENAI_API_KEY,
permissionMode: "acceptEdits", // Maps to "workspace-write" sandbox mode
model: "gpt-4o",
});
// Stream call
const { events } = await codex.runStream({ query: "Analyze code" });
// Non-stream call
const result = await codex.run({ query: "Analyze code" });CodexClient Configuration Options:
| Option | Type | Description |
|------|------|------|
| apiKey | string | OpenAI API key |
| permissionMode | "auto" \| "default" \| "acceptEdits" \| "bypassPermissions" | Permission mode (maps to sandbox mode) |
| model | string | Model name (e.g., "gpt-4o") |
| systemPrompt | string | System prompt |
| mcpServers | Record<string, McpServerConfig> | MCP server configuration |
| addDirs | string[] | Additional directories for access |
| workingDirectory | string | Working directory |
| env | Record<string, string> | Environment variables (e.g., CODEX_HOME) |
If env.CODEX_HOME is not set, a temporary directory is created under {TMPDIR}/unagent/sessions/<id>.
Codex Permission Mode Mapping:
The permissionMode option maps to Codex sandbox modes as follows:
| permissionMode | Codex sandboxMode | Description |
|-----------------|-------------------|-------------|
| "auto" / "default" / "dontAsk" / "plan" | "read-only" | Read-only access (safest) |
| "acceptEdits" | "workspace-write" | Can write to workspace and additional directories |
| "bypassPermissions" | "danger-full-access" | No sandbox, full system access (riskiest) |
Available Tools:
Read- Read filesWrite- Create new filesEdit- Edit existing filesBash- Run commandsGlob- Find filesGrep- Search file contentsWebSearch- Web searchWebFetch- Fetch web contentTask- Call sub agentAskUserQuestion- Ask user questions
Event Types
All events inherit from BaseEvent and include timestamp, clientType, and optional sessionId.
SessionStartEvent
{
type: "session.start",
timestamp: number,
clientType: ClientType,
sessionId: string,
params?: { query?: string, ... }
}SessionEndEvent
{
type: "session.end",
timestamp: number,
clientType: ClientType,
sessionId?: string,
reason?: string
}ItemStartEvent
{
type: "item.start",
timestamp: number,
clientType: ClientType,
message: Message
}ItemChunkEvent
{
type: "item.chunk",
timestamp: number,
clientType: ClientType,
message: Message
}ItemEndEvent
{
type: "item.end",
timestamp: number,
clientType: ClientType,
message: Message
}ErrorEvent
{
type: "error",
timestamp: number,
clientType: ClientType,
error: Error,
context?: { stage?: string, ... }
}Advanced Usage
Adding MCP Servers
const claude = new ClaudeClient({
apiKey: process.env.ANTHROPIC_API_KEY,
allowedTools: ["Read", "Bash"],
mcpServers: {
playwright: {
command: "npx",
args: ["@playwright/mcp@latest"],
},
},
});Using Hooks
const claude = new ClaudeClient({
apiKey: process.env.ANTHROPIC_API_KEY,
hooks: {
PostToolUse: [{
matcher: "Edit|Write",
hooks: [async (input) => {
// Log file modifications
console.log("File modified:", input.tool_input?.file_path);
return {};
}],
}],
},
});Session Resumption
let sessionId: string | undefined;
// First query
for await (const message of query(...)) {
if (message.type === "system" && message.subtype === "init") {
sessionId = message.session_id;
}
}
// Resume session
const resumed = new ClaudeClient({
apiKey: process.env.ANTHROPIC_API_KEY,
resume: sessionId,
});Development
Build
npm run buildExamples
See examples/basic-usage.ts for complete usage examples.
File Structure
src/
├── events/ # Event system core
│ ├── event-types.ts # Event type definitions
│ ├── event-bus.ts # Event bus implementation
│ └── index.ts # Export module
├── messages/ # Message system
│ ├── message-types.ts # Message type definitions
│ ├── message-utils.ts # Message utility functions
│ └── index.ts # Export module
├── client/ # Client abstraction layer
│ ├── base-client.ts # Base client interface and abstract class
│ ├── codex/ # Codex client implementation
│ └── claude.ts # Claude Agent SDK implementation
├── tools/ # Tool system
├── agents/ # Agent system
└── index.ts # Main entry fileMigration Guide
If migrating from the old @anthropic-ai/sdk, the main changes are:
- No need to call
messages.create()directly - Agent SDK automatically handles tool execution throughquery()function - Configuration options changed - Use
allowedToolsandpermissionModeinstead ofmodel,maxTokens, etc. - Event format changed - New SDK returns different event formats, which are automatically converted to unified format
License
MIT
