# How to Integrate Cloud Agents Like Codex Cloud, Devin, and Jules with Task Persistence in OmniRoute

> Integrate cloud agents like Codex Cloud, Devin, and Jules with task persistence in OmniRoute. Run long AI tasks remotely with automatic SQLite persistence and a pluggable registry.

- Repository: [Diego Rodrigues de Sa e Souza/OmniRoute](https://github.com/diegosouzapw/OmniRoute)
- Tags: how-to-guide
- Published: 2026-08-03

---

**OmniRoute provides a cloud-agent framework that lets you run long-running AI tasks on remote providers with automatic SQLite persistence, stateless API interactions, and a pluggable registry system for adding new agents.**

The framework centers on a clean abstraction layer that separates provider-specific HTTP logic from OmniRoute's internal task lifecycle. This guide walks through the architecture, persistence mechanism, and practical code examples for integrating **Codex Cloud**, **Devin**, **Jules**, and other cloud agents into your workflows.

## Cloud Agent Architecture Overview

OmniRoute's cloud-agent system is built on four core components in `src/lib/cloudAgent/`:

| Component | Purpose | Key File |
|-----------|---------|----------|
| **CloudAgentBase** | Abstract class defining the contract all agents must implement | [`baseAgent.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/baseAgent.ts) |
| **Agent implementations** | Provider-specific classes that translate OmniRoute calls to HTTP APIs | [`agents/codex.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/agents/codex.ts), [`agents/devin.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/agents/devin.ts) |
| **Registry** | Lazily-loaded singleton map of available agents with discovery helpers | [`registry.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/registry.ts) |
| **Persistence layer** | Encrypted credential storage and SQLite task tracking | [`credentials.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/credentials.ts), [`db.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/db.ts) |

The shared TypeScript contracts in [`types.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/types.ts) ensure every agent returns normalized data shapes regardless of provider differences.

## The Agent Contract: CloudAgentBase

All cloud agents inherit from `CloudAgentBase` in [`src/lib/cloudAgent/baseAgent.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/cloudAgent/baseAgent.ts). This abstract class enforces a consistent interface across Codex Cloud, Devin, Jules, and future providers:

```typescript
export abstract class CloudAgentBase {
  abstract readonly providerId: string;
  abstract readonly baseUrl: string;

  // Create a new task on the remote provider
  abstract createTask(
    params: CreateTaskParams,
    credentials: AgentCredentials
  ): Promise<CloudAgentTask>;

  // Poll for current status
  abstract getStatus(
    externalId: string,
    credentials: AgentCredentials
  ): Promise<GetStatusResult>;

  // Approve a generated plan (optional support)
  abstract approvePlan(
    externalId: string,
    credentials: AgentCredentials
  ): Promise<void>;

  // Send follow-up messages to running tasks
  abstract sendMessage(
    externalId: string,
    message: string,
    credentials: AgentCredentials
  ): Promise<CloudAgentActivity>;

  // List repositories the agent can access
  abstract listSources(
    credentials: AgentCredentials
  ): Promise<{ name: string; url: string; branch?: string }[]>;
}

```

Concrete implementations in [`agents/codex.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/agents/codex.ts) and [`agents/devin.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/agents/devin.ts) handle provider-specific authentication, endpoint mapping, and response parsing while returning standardized `CloudAgentTask` objects.

## Task Persistence in SQLite

Task persistence happens automatically when you call `createTask`. Here's the lifecycle:

1. **ID generation**: The agent creates an internal OmniRoute ID (`task_<timestamp>_<random>`) via `generateTaskId()` in [`baseAgent.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/baseAgent.ts)
2. **Remote creation**: The agent calls the provider's API and receives an `externalId`
3. **Database write**: The full task record is stored in the `cloudAgentTask` table via [`src/lib/cloudAgent/db.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/cloudAgent/db.ts)

The persisted `CloudAgentTask` contains:

- `id` — OmniRoute's local identifier
- `externalId` — the provider's task identifier
- `providerId` — which agent created the task (`codex-cloud`, `devin`, `jules`, etc.)
- `status` — normalized state (`queued`, `running`, `completed`, `failed`)
- `prompt`, `source`, `options` — the original input payload
- `createdAt` / `updatedAt` — timestamps for tracking

The database schema is established by migration [`061_cloud_agent_credentials.sql`](https://github.com/diegosouzapw/OmniRoute/blob/main/061_cloud_agent_credentials.sql). Because tasks live in OmniRoute's core SQLite database at `src/lib/db/`, they survive server restarts and remain queryable from any UI component.

### Credential Security

API keys are stored encrypted via [`src/lib/cloudAgent/credentials.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/cloudAgent/credentials.ts). The `getCredentialsForProvider()` function retrieves keys by provider ID, ensuring each HTTP request uses the correct authentication without exposing secrets in memory longer than necessary.

## Discovering Available Agents

The registry pattern in [`src/lib/cloudAgent/registry.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/cloudAgent/registry.ts) provides lazy initialization and clean access to agent instances:

```typescript
import { getAvailableAgents, getAgent } from '@/lib/cloudAgent/registry.ts';

// List all registered cloud agents
const agents = getAvailableAgents();
// → ["jules", "devin", "codex-cloud", "cursor-cloud"]
console.log('Available providers:', agents);

// Obtain a concrete agent instance
const codex = getAgent('codex-cloud');
if (!codex) throw new Error('Provider not registered');

```

The registry maintains a singleton map, so repeated calls to `getAgent()` return the same instance without re-instantiation overhead.

## Creating Tasks with Codex Cloud

Here's a complete example for spinning up a Codex Cloud task with persistence:

```typescript
import { getAgent } from '@/lib/cloudAgent/registry.ts';
import type { CreateTaskParams } from '@/lib/cloudAgent/baseAgent.ts';

async function runCodexTask() {
  const agent = getAgent('codex-cloud');
  if (!agent) throw new Error('Codex Cloud agent not found');

  // Credentials are typically loaded from encrypted DB storage
  const creds = { apiKey: process.env.CODEX_API_KEY! };

  const params: CreateTaskParams = {
    prompt: 'Write a TypeScript utility that formats dates',
    source: { repoUrl: 'https://github.com/example/repo' },
    options: { environment: { NODE_ENV: 'production' } },
  };

  // This creates the remote task AND persists to SQLite
  const task = await agent.createTask(params, creds);
  
  console.log('Created task', task.id);
  console.log('Remote ID:', task.externalId);
  console.log('Initial status:', task.status);
  
  return task;
}

```

The `createTask` method in [`agents/codex.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/agents/codex.ts) handles the Codex Cloud HTTP API specifics while [`baseAgent.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/baseAgent.ts) helpers normalize the response into `CloudAgentTask`.

## Polling Task Status

Since cloud agents run asynchronously, you'll poll `getStatus()` to track progress. The operation is stateless—each call fetches fresh state from the provider:

```typescript
import { getAgent } from '@/lib/cloudAgent/registry.ts';
import type { GetStatusResult } from '@/lib/cloudAgent/baseAgent.ts';

async function pollUntilComplete(externalId: string) {
  const agent = getAgent('codex-cloud')!;
  const creds = { apiKey: process.env.CODEX_API_KEY! };

  while (true) {
    const status: GetStatusResult = await agent.getStatus(externalId, creds);
    
    console.log('Status:', status.status);
    console.log('Activities:', status.activities?.length || 0);

    if (status.status === 'completed') {
      console.log('Result:', status.result);
      return status;
    }
    
    if (status.status === 'failed') {
      console.error('Task failed:', status.error);
      throw new Error(status.error?.message || 'Unknown failure');
    }

    await new Promise(r => setTimeout(r, 5000)); // 5-second poll interval
  }
}

```

The `GetStatusResult` includes normalized `status` values and an `activities` array that UI components can render directly.

## Sending Messages to Running Tasks

Both Devin and Codex Cloud support follow-up messages for interactive workflows:

```typescript
import { getAgent } from '@/lib/cloudAgent/registry.ts';

async function sendDevinMessage(externalId: string, message: string) {
  const agent = getAgent('devin')!;
  const creds = { apiKey: process.env.DEVIN_API_KEY! };

  const activity = await agent.sendMessage(externalId, message, creds);
  
  console.log('Message sent, activity ID:', activity.id);
  console.log('Timestamp:', activity.timestamp);
  
  return activity;
}

```

The `sendMessage` implementation in [`agents/devin.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/agents/devin.ts) translates to Devin's conversation API, while [`agents/codex.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/agents/codex.ts) maps to Codex Cloud's equivalent endpoint.

## Plan Approval Flow

Some future agents (like a planned Cursor Cloud integration) require explicit plan approval before execution. The `approvePlan` method in `CloudAgentBase` supports this:

```typescript
// For agents that auto-plan (Codex Cloud, Devin), this throws or no-ops
await agent.approvePlan(externalId, creds);

// For agents with manual approval, this releases the task to execute

```

Check `agent.approvePlan` implementation details in your specific agent file—Codex Cloud and Devin currently auto-approve plans on creation.

## Working with Persisted Credentials

For internal tooling or custom integrations, access encrypted credentials directly:

```typescript
import { getCredentialsForProvider } from '@/lib/cloudAgent/credentials.ts';

async function loadDevinConfig() {
  const creds = await getCredentialsForProvider('devin');
  
  // Returns: { apiKey: string, baseUrl?: string }
  return {
    apiKey: creds.apiKey,
    endpoint: creds.baseUrl || 'https://api.devin.ai/v1',
  };
}

```

The [`credentials.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/credentials.ts) module handles encryption/decryption transparently using OmniRoute's internal key management.

## Key Implementation Files

| File | Role | Link |
|------|------|------|
| [`src/lib/cloudAgent/baseAgent.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/cloudAgent/baseAgent.ts) | Abstract contract, ID generation, status mapping | [View source](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/cloudAgent/baseAgent.ts) |
| [`src/lib/cloudAgent/agents/codex.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/cloudAgent/agents/codex.ts) | Codex Cloud HTTP implementation | [View source](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/cloudAgent/agents/codex.ts) |
| [`src/lib/cloudAgent/agents/devin.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/cloudAgent/agents/devin.ts) | Devin HTTP implementation | [View source](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/cloudAgent/agents/devin.ts) |
| [`src/lib/cloudAgent/registry.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/cloudAgent/registry.ts) | Agent discovery and singleton access | [View source](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/cloudAgent/registry.ts) |
| [`src/lib/cloudAgent/types.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/cloudAgent/types.ts) | Shared TypeScript interfaces | [View source](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/cloudAgent/types.ts) |
| [`src/lib/cloudAgent/credentials.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/cloudAgent/credentials.ts) | Encrypted API key storage | [View source](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/cloudAgent/credentials.ts) |
| [`src/lib/cloudAgent/db.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/cloudAgent/db.ts) | SQLite task persistence helpers | [View source](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/cloudAgent/db.ts) |
| [`db/migrations/061_cloud_agent_credentials.sql`](https://github.com/diegosouzapw/OmniRoute/blob/main/db/migrations/061_cloud_agent_credentials.sql) | Schema migration for credentials table | [View source](https://github.com/diegosouzapw/OmniRoute/blob/main/db/migrations/061_cloud_agent_credentials.sql) |

## Summary

- **CloudAgentBase** in [`src/lib/cloudAgent/baseAgent.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/cloudAgent/baseAgent.ts) provides the abstract contract that all providers implement
- **Registry pattern** via [`registry.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/registry.ts) enables discovery of `jules`, `devin`, `codex-cloud`, and `cursor-cloud` agents
- **Automatic persistence** stores every task in SQLite with encrypted credentials, surviving server restarts
- **Stateless polling** lets you check status and send messages without maintaining long-lived connections
- **Provider implementations** in [`agents/codex.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/agents/codex.ts) and [`agents/devin.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/agents/devin.ts) handle HTTP specifics while returning normalized data

## Frequently Asked Questions

### How do I add a new cloud agent provider to OmniRoute?

Create a new file in `src/lib/cloudAgent/agents/` that extends `CloudAgentBase` from [`baseAgent.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/baseAgent.ts). Implement all abstract methods—`createTask`, `getStatus`, `approvePlan`, `sendMessage`, and `listSources`—translating between your provider's HTTP API and OmniRoute's internal types. Register the agent in [`registry.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/registry.ts) by adding it to the `AGENTS` map with a unique `providerId`.

### Where are cloud agent API keys stored?

API keys are encrypted and stored in SQLite via [`src/lib/cloudAgent/credentials.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/cloudAgent/credentials.ts), using the schema defined in migration [`061_cloud_agent_credentials.sql`](https://github.com/diegosouzapw/OmniRoute/blob/main/061_cloud_agent_credentials.sql). The encryption uses OmniRoute's internal key management, and credentials are retrieved by `providerId` when making API calls.

### Can I run multiple cloud agent tasks simultaneously?

Yes. Each task receives a unique internal ID generated by `generateTaskId()` in [`baseAgent.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/baseAgent.ts), and the SQLite persistence layer tracks them independently. Poll each task's status separately using its `externalId`, or query the database directly for a global view of running tasks.

### What happens if OmniRoute restarts while a task is running?

Tasks survive restarts because all state lives in the `cloudAgentTask` SQLite table managed by [`src/lib/cloudAgent/db.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/cloudAgent/db.ts). On startup, you can query incomplete tasks and resume polling their status—no manual recovery needed. The encrypted credentials also persist, so resumed tasks authenticate automatically.