> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/ComposioHQ/agent-orchestrator/llms.txt
> Use this file to discover all available pages before exploring further.

# Creating Plugins

> Guide to developing custom plugins for Agent Orchestrator

## Plugin Development Guide

Creating a plugin for Agent Orchestrator is straightforward: implement one of the core interfaces and export it as a `PluginModule`. This guide walks you through the process.

## Prerequisites

Before creating a plugin:

1. **Understand the interface** - Read `packages/core/src/types.ts` to understand the interface you'll implement
2. **Study examples** - Look at existing plugins in `packages/plugins/` for patterns
3. **Set up TypeScript** - Use TypeScript with strict mode enabled

## Plugin Module Structure

Every plugin must export a `PluginModule` with two properties:

<Steps>
  <Step title="Manifest">
    Plugin metadata describing the plugin:

    ```typescript theme={null}
    export const manifest = {
      name: "my-plugin",              // Plugin name (unique within slot)
      slot: "runtime" as const,       // Which slot this plugin fills
      description: "My custom plugin", // Human-readable description
      version: "0.1.0",               // Semantic version
    };
    ```
  </Step>

  <Step title="Create Function">
    Factory function that returns the plugin implementation:

    ```typescript theme={null}
    export function create(config?: Record<string, unknown>): Runtime {
      // Optional: extract config values
      const timeout = config?.timeout as number ?? 30_000;
      
      // Return interface implementation
      return {
        name: "my-plugin",
        async create(config) { /* ... */ },
        async destroy(handle) { /* ... */ },
        // ... implement all interface methods
      };
    }
    ```
  </Step>

  <Step title="Default Export with Type Safety">
    Export the module with type checking:

    ```typescript theme={null}
    export default { manifest, create } satisfies PluginModule<Runtime>;
    ```

    The `satisfies` operator ensures compile-time type safety without type widening.
  </Step>
</Steps>

## Complete Plugin Template

Here's a complete plugin template for a Runtime plugin:

```typescript theme={null}
import type { 
  PluginModule, 
  Runtime,
  RuntimeCreateConfig,
  RuntimeHandle,
  RuntimeMetrics,
  AttachInfo,
} from "@composio/ao-core";
import { execFile } from "node:child_process";
import { promisify } from "node:util";

const execFileAsync = promisify(execFile);

// =============================================================================
// Manifest
// =============================================================================

export const manifest = {
  name: "my-runtime",
  slot: "runtime" as const,
  description: "My custom runtime plugin",
  version: "0.1.0",
};

// =============================================================================
// Implementation
// =============================================================================

export function create(config?: Record<string, unknown>): Runtime {
  // Extract and validate config
  const timeout = typeof config?.timeout === "number" ? config.timeout : 30_000;
  
  return {
    name: "my-runtime",

    async create(config: RuntimeCreateConfig): Promise<RuntimeHandle> {
      // 1. Create execution environment
      // 2. Launch the agent with config.launchCommand
      // 3. Return handle for future operations
      
      return {
        id: "unique-session-id",
        runtimeName: "my-runtime",
        data: {
          createdAt: Date.now(),
          // Store runtime-specific data here
        },
      };
    },

    async destroy(handle: RuntimeHandle): Promise<void> {
      // Clean up the execution environment
      // Best-effort cleanup - don't throw if already dead
    },

    async sendMessage(handle: RuntimeHandle, message: string): Promise<void> {
      // Send a message to the running agent
      // This is how the orchestrator communicates with agents
    },

    async getOutput(handle: RuntimeHandle, lines = 50): Promise<string> {
      // Capture recent output from the session
      // Used for activity detection and debugging
      return "";
    },

    async isAlive(handle: RuntimeHandle): Promise<boolean> {
      // Check if the session environment is still running
      return false;
    },

    async getMetrics(handle: RuntimeHandle): Promise<RuntimeMetrics> {
      // Optional: return resource usage metrics
      return {
        uptimeMs: Date.now() - (handle.data.createdAt as number),
      };
    },

    async getAttachInfo(handle: RuntimeHandle): Promise<AttachInfo> {
      // Optional: return info for Terminal plugin to attach humans
      return {
        type: "process",
        target: handle.id,
        command: `attach-to ${handle.id}`,
      };
    },
  };
}

// =============================================================================
// Plugin Export
// =============================================================================

export default { manifest, create } satisfies PluginModule<Runtime>;
```

## Interface Requirements by Slot

### Runtime Plugin

**Required methods:**

* `create(config)` - Create a new execution environment
* `destroy(handle)` - Destroy the environment
* `sendMessage(handle, message)` - Send input to the agent
* `getOutput(handle, lines?)` - Capture recent output
* `isAlive(handle)` - Check if environment is alive

**Optional methods:**

* `getMetrics(handle)` - Return resource metrics
* `getAttachInfo(handle)` - Return attachment info for Terminal plugin

<Accordion title="Implementation Example: tmux Runtime">
  ```typescript theme={null}
  // packages/plugins/runtime-tmux/src/index.ts
  export function create(): Runtime {
    return {
      name: "tmux",
      
      async create(config: RuntimeCreateConfig): Promise<RuntimeHandle> {
        const sessionName = config.sessionId;
        
        // Build environment flags
        const envArgs: string[] = [];
        for (const [key, value] of Object.entries(config.environment ?? {})) {
          envArgs.push("-e", `${key}=${value}`);
        }
        
        // Create tmux session
        await tmux("new-session", "-d", "-s", sessionName, 
                   "-c", config.workspacePath, ...envArgs);
        
        // Send launch command
        await tmux("send-keys", "-t", sessionName, 
                   config.launchCommand, "Enter");
        
        return {
          id: sessionName,
          runtimeName: "tmux",
          data: { createdAt: Date.now() },
        };
      },
      
      async destroy(handle: RuntimeHandle): Promise<void> {
        try {
          await tmux("kill-session", "-t", handle.id);
        } catch {
          // Session may already be dead
        }
      },
      
      // ... other methods
    };
  }
  ```
</Accordion>

### Agent Plugin

**Required methods:**

* `getLaunchCommand(config)` - Generate shell command to launch agent
* `getEnvironment(config)` - Return environment variables
* `detectActivity(terminalOutput)` - Classify activity from output (deprecated)
* `getActivityState(session, threshold?)` - Get current activity state
* `isProcessRunning(handle)` - Check if agent process is alive
* `getSessionInfo(session)` - Extract summary and cost info

**Optional methods:**

* `getRestoreCommand(session, project)` - Generate command to resume session
* `postLaunchSetup(session)` - Run setup after agent launches
* `setupWorkspaceHooks(workspacePath, config)` - Install hooks for metadata updates

**Properties:**

* `name` - Plugin name
* `processName` - Process name to look for (e.g., "claude", "aider")
* `promptDelivery` - "inline" or "post-launch" for prompt delivery

<Accordion title="Implementation Example: Agent Activity Detection">
  ```typescript theme={null}
  // Activity detection using agent-native mechanisms
  async getActivityState(
    session: Session,
    readyThresholdMs = DEFAULT_READY_THRESHOLD_MS
  ): Promise<ActivityDetection | null> {
    // Check if process is running
    if (!session.runtimeHandle) {
      return { state: "exited", timestamp: new Date() };
    }
    
    const running = await this.isProcessRunning(session.runtimeHandle);
    if (!running) {
      return { state: "exited", timestamp: new Date() };
    }
    
    // Read agent's session file (JSONL, SQLite, etc.)
    const sessionFile = await findLatestSessionFile(session.workspacePath);
    if (!sessionFile) return null;
    
    const entry = await readLastJsonlEntry(sessionFile);
    if (!entry) return null;
    
    const ageMs = Date.now() - entry.modifiedAt.getTime();
    const timestamp = entry.modifiedAt;
    
    // Classify based on last entry type
    switch (entry.lastType) {
      case "user":
      case "tool_use":
        return { state: ageMs > readyThresholdMs ? "idle" : "active", timestamp };
      
      case "assistant":
      case "result":
        return { state: ageMs > readyThresholdMs ? "idle" : "ready", timestamp };
      
      case "permission_request":
        return { state: "waiting_input", timestamp };
      
      case "error":
        return { state: "blocked", timestamp };
      
      default:
        return { state: "active", timestamp };
    }
  }
  ```
</Accordion>

### Workspace Plugin

**Required methods:**

* `create(config)` - Create isolated workspace
* `destroy(workspacePath)` - Remove workspace
* `list(projectId)` - List existing workspaces

**Optional methods:**

* `postCreate(info, project)` - Run hooks after creation (symlinks, installs)
* `exists(workspacePath)` - Check if workspace is valid
* `restore(config, workspacePath)` - Recreate workspace from existing data

<Accordion title="Implementation Example: Git Worktree">
  ```typescript theme={null}
  export function create(config?: Record<string, unknown>): Workspace {
    const worktreeBaseDir = config?.worktreeDir 
      ? expandPath(config.worktreeDir as string)
      : join(homedir(), ".worktrees");
    
    return {
      name: "worktree",
      
      async create(cfg: WorkspaceCreateConfig): Promise<WorkspaceInfo> {
        const repoPath = expandPath(cfg.project.path);
        const worktreePath = join(worktreeBaseDir, cfg.projectId, cfg.sessionId);
        
        // Fetch latest
        await git(repoPath, "fetch", "origin", "--quiet");
        
        const baseRef = `origin/${cfg.project.defaultBranch}`;
        
        // Create worktree with new branch
        await git(repoPath, "worktree", "add", "-b", cfg.branch, 
                  worktreePath, baseRef);
        
        return {
          path: worktreePath,
          branch: cfg.branch,
          sessionId: cfg.sessionId,
          projectId: cfg.projectId,
        };
      },
      
      async destroy(workspacePath: string): Promise<void> {
        const gitCommonDir = await git(workspacePath, "rev-parse", 
                                       "--git-common-dir");
        const repoPath = resolve(gitCommonDir, "..");
        await git(repoPath, "worktree", "remove", "--force", workspacePath);
      },
      
      // ... other methods
    };
  }
  ```
</Accordion>

### Tracker Plugin

**Required methods:**

* `getIssue(identifier, project)` - Fetch issue details
* `isCompleted(identifier, project)` - Check if issue is closed
* `issueUrl(identifier, project)` - Generate issue URL
* `branchName(identifier, project)` - Generate branch name from issue
* `generatePrompt(identifier, project)` - Generate agent prompt

**Optional methods:**

* `issueLabel(url, project)` - Extract human-readable label from URL
* `listIssues(filters, project)` - List issues with filters
* `updateIssue(identifier, update, project)` - Update issue state
* `createIssue(input, project)` - Create new issue

### SCM Plugin

**Required methods:**

* `detectPR(session, project)` - Detect PR by branch name
* `getPRState(pr)` - Get PR state (open/merged/closed)
* `mergePR(pr, method?)` - Merge a PR
* `closePR(pr)` - Close PR without merging
* `getCIChecks(pr)` - Get individual CI checks
* `getCISummary(pr)` - Get overall CI status
* `getReviews(pr)` - Get all reviews
* `getReviewDecision(pr)` - Get overall review decision
* `getPendingComments(pr)` - Get unresolved comments
* `getAutomatedComments(pr)` - Get bot comments
* `getMergeability(pr)` - Check merge readiness

**Optional methods:**

* `getPRSummary(pr)` - Get PR summary with stats

### Notifier Plugin

**Required methods:**

* `notify(event)` - Push notification to human

**Optional methods:**

* `notifyWithActions(event, actions)` - Notification with action buttons
* `post(message, context?)` - Post to channel (for team notifiers)

<Accordion title="Implementation Example: Desktop Notifier">
  ```typescript theme={null}
  export function create(config?: Record<string, unknown>): Notifier {
    const soundEnabled = typeof config?.sound === "boolean" ? config.sound : true;
    
    return {
      name: "desktop",
      
      async notify(event: OrchestratorEvent): Promise<void> {
        const title = `Agent Orchestrator [${event.sessionId}]`;
        const message = event.message;
        const sound = event.priority === "urgent" && soundEnabled;
        
        if (platform() === "darwin") {
          const safeTitle = escapeAppleScript(title);
          const safeMessage = escapeAppleScript(message);
          const soundClause = sound ? ' sound name "default"' : "";
          const script = `display notification "${safeMessage}" with title "${safeTitle}"${soundClause}`;
          await execFileAsync("osascript", ["-e", script]);
        } else if (platform() === "linux") {
          const args = event.priority === "urgent" ? ["--urgency=critical"] : [];
          args.push(title, message);
          await execFileAsync("notify-send", args);
        }
      },
    };
  }
  ```
</Accordion>

### Terminal Plugin

**Required methods:**

* `openSession(session)` - Open single session for human
* `openAll(sessions)` - Open all sessions for a project

**Optional methods:**

* `isSessionOpen(session)` - Check if session is already open

## Code Conventions

<Warning>
  These conventions are enforced by ESLint and Prettier. Follow them to avoid CI failures.
</Warning>

### TypeScript Standards

1. **ESM modules** - Use `"type": "module"` in package.json
2. **`.js` extensions** - Include in all local imports: `import { foo } from "./bar.js"`
3. **`node:` prefix** - Use for built-ins: `import { readFile } from "node:fs/promises"`
4. **Strict mode** - Enable `"strict": true` in tsconfig
5. **Type imports** - Use `import type` for type-only imports
6. **No `any`** - Use `unknown` + type guards instead
7. **Prefer `const`** - Use `let` only for reassignment, never `var`

### Security Requirements

<Steps>
  <Step title="Always use execFile">
    **NEVER use `exec`** - it's vulnerable to shell injection.

    ```typescript theme={null}
    // GOOD
    import { execFile } from "node:child_process";
    import { promisify } from "node:util";
    const execFileAsync = promisify(execFile);

    await execFileAsync("git", ["branch", branchName], { timeout: 30_000 });

    // BAD - shell injection risk
    exec(`git branch ${branchName}`); // branchName could contain ; rm -rf /
    ```
  </Step>

  <Step title="Always add timeouts">
    Set timeouts for all external commands:

    ```typescript theme={null}
    await execFileAsync("gh", ["pr", "view", prNumber], { 
      timeout: 30_000,
      maxBuffer: 10 * 1024 * 1024,
    });
    ```
  </Step>

  <Step title="Never interpolate user input">
    Pass user input as array arguments, not string templates:

    ```typescript theme={null}
    // GOOD
    await execFileAsync("git", ["checkout", "-b", branchName]);

    // BAD
    await execFileAsync("sh", ["-c", `git checkout -b ${branchName}`]);
    ```
  </Step>

  <Step title="Validate external data">
    Guard against malformed API responses:

    ```typescript theme={null}
    try {
      const data: unknown = JSON.parse(raw);
      if (typeof data !== "object" || data === null) {
        throw new Error("Invalid response format");
      }
      // ... validate structure
    } catch (err) {
      throw new Error(`Failed to parse response: ${err}`);
    }
    ```
  </Step>
</Steps>

### Error Handling

1. **Throw typed errors** - Don't return error codes
2. **Plugins throw** - If they can't do their job
3. **Core services catch** - Handle plugin errors gracefully
4. **Wrap JSON.parse** - Corrupted data shouldn't crash
5. **Best-effort cleanup** - Don't throw during cleanup/destroy

```typescript theme={null}
async destroy(handle: RuntimeHandle): Promise<void> {
  try {
    await execFileAsync("docker", ["rm", "-f", handle.id]);
  } catch {
    // Container may already be gone - that's fine
  }
}
```

## Testing Your Plugin

### Unit Tests

Create tests in `__tests__/` or co-located `.test.ts` files:

```typescript theme={null}
import { describe, it, expect } from "vitest";
import { create, manifest } from "./index.js";

describe("my-plugin", () => {
  it("exports correct manifest", () => {
    expect(manifest.name).toBe("my-plugin");
    expect(manifest.slot).toBe("runtime");
  });
  
  it("creates valid instance", () => {
    const plugin = create();
    expect(plugin.name).toBe("my-plugin");
  });
  
  // Add more tests...
});
```

### Integration Testing

1. **Local testing** - Install your plugin locally:
   ```bash theme={null}
   cd ~/my-plugin
   pnpm link --global

   cd ~/agent-orchestrator
   pnpm link --global @ao-plugin/runtime-my-plugin
   ```

2. **Configure** - Add to `agent-orchestrator.yaml`:
   ```yaml theme={null}
   defaults:
     runtime: my-plugin
   ```

3. **Test spawn** - Spawn a session:
   ```bash theme={null}
   ao spawn my-project 123
   ao status
   ```

## Publishing Your Plugin

### Package Structure

```
ao-plugin-runtime-my-plugin/
├── package.json
├── tsconfig.json
├── src/
│   └── index.ts
├── dist/           # Built output
│   └── index.js
└── README.md
```

### package.json

```json theme={null}
{
  "name": "@ao-plugin/runtime-my-plugin",
  "version": "0.1.0",
  "type": "module",
  "main": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "default": "./dist/index.js"
    }
  },
  "scripts": {
    "build": "tsc",
    "test": "vitest"
  },
  "dependencies": {
    "@composio/ao-core": "^0.1.0"
  },
  "devDependencies": {
    "typescript": "^5.3.0",
    "vitest": "^1.0.0"
  },
  "peerDependencies": {
    "@composio/ao-core": "^0.1.0"
  }
}
```

### Publishing to npm

<Steps>
  <Step title="Build">
    ```bash theme={null}
    pnpm build
    ```
  </Step>

  <Step title="Test">
    ```bash theme={null}
    pnpm test
    ```
  </Step>

  <Step title="Publish">
    ```bash theme={null}
    npm publish --access public
    ```
  </Step>
</Steps>

### Using Published Plugin

Users can install and use your plugin:

```bash theme={null}
pnpm add @ao-plugin/runtime-my-plugin
```

```yaml theme={null}
# agent-orchestrator.yaml
defaults:
  runtime: my-plugin
```

## Plugin Checklist

Before publishing, verify:

* [ ] Implements complete interface (no missing methods)
* [ ] Uses `satisfies PluginModule<T>` for type safety
* [ ] Follows security conventions (execFile, timeouts, no interpolation)
* [ ] Includes error handling (try/catch, typed errors)
* [ ] Has unit tests covering core functionality
* [ ] Tested locally with real Agent Orchestrator config
* [ ] Has README with usage examples
* [ ] Uses semantic versioning
* [ ] Declares `@composio/ao-core` as peer dependency

## Common Patterns

### Config Extraction

```typescript theme={null}
export function create(config?: Record<string, unknown>): MyPlugin {
  // Extract with defaults
  const timeout = typeof config?.timeout === "number" ? config.timeout : 30_000;
  const apiKey = typeof config?.apiKey === "string" ? config.apiKey : process.env.MY_API_KEY;
  
  if (!apiKey) {
    throw new Error("API key required: set MY_API_KEY environment variable");
  }
  
  return { /* ... */ };
}
```

### Process Execution

```typescript theme={null}
import { execFile } from "node:child_process";
import { promisify } from "node:util";

const execFileAsync = promisify(execFile);

async function runCommand(args: string[]): Promise<string> {
  try {
    const { stdout } = await execFileAsync("mycommand", args, { 
      timeout: 30_000,
      maxBuffer: 10 * 1024 * 1024,
    });
    return stdout.trim();
  } catch (err) {
    throw new Error(`Command failed: ${(err as Error).message}`, { cause: err });
  }
}
```

### Caching

```typescript theme={null}
let cache: { data: string; timestamp: number } | null = null;
const CACHE_TTL_MS = 5_000;

async function getCachedData(): Promise<string> {
  const now = Date.now();
  if (cache && now - cache.timestamp < CACHE_TTL_MS) {
    return cache.data;
  }
  
  const data = await fetchExpensiveData();
  cache = { data, timestamp: now };
  return data;
}
```

### Path Safety

```typescript theme={null}
const SAFE_PATH_SEGMENT = /^[a-zA-Z0-9_-]+$/;

function assertSafePathSegment(value: string, label: string): void {
  if (!SAFE_PATH_SEGMENT.test(value)) {
    throw new Error(`Invalid ${label} "${value}": must match ${SAFE_PATH_SEGMENT}`);
  }
}

// Usage
assertSafePathSegment(sessionId, "sessionId");
const path = join(baseDir, sessionId); // Safe - no directory traversal
```

## Resources

<CardGroup cols={2}>
  <Card title="Core Types" icon="file-code" href="https://github.com/ComposioHQ/agent-orchestrator/blob/main/packages/core/src/types.ts">
    Complete interface definitions
  </Card>

  <Card title="Example Plugins" icon="folder-open" href="https://github.com/ComposioHQ/agent-orchestrator/tree/main/packages/plugins">
    Browse built-in plugin implementations
  </Card>

  <Card title="CLAUDE.md" icon="book" href="https://github.com/ComposioHQ/agent-orchestrator/blob/main/CLAUDE.md">
    Code conventions and architecture
  </Card>

  <Card title="Community Plugins" icon="users" href="https://github.com/topics/agent-orchestrator-plugin">
    Discover community plugins
  </Card>
</CardGroup>

## Getting Help

If you run into issues:

1. Check existing plugin implementations for patterns
2. Review `packages/core/src/types.ts` for interface details
3. Open an issue on GitHub for questions
4. Join discussions in the community

<Info>
  Contributions are welcome! Consider submitting your plugin to the official plugin repository via pull request.
</Info>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.