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

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
Agent implementations Provider-specific classes that translate OmniRoute calls to HTTP APIs agents/codex.ts, agents/devin.ts
Registry Lazily-loaded singleton map of available agents with discovery helpers registry.ts
Persistence layer Encrypted credential storage and SQLite task tracking credentials.ts, db.ts

The shared TypeScript contracts in 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. This abstract class enforces a consistent interface across Codex Cloud, Devin, Jules, and future providers:

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 and 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
  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

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. 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. 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 provides lazy initialization and clean access to agent instances:

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:

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 handles the Codex Cloud HTTP API specifics while 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:

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:

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 translates to Devin's conversation API, while 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:

// 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:

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 module handles encryption/decryption transparently using OmniRoute's internal key management.

Key Implementation Files

File Role Link
src/lib/cloudAgent/baseAgent.ts Abstract contract, ID generation, status mapping View source
src/lib/cloudAgent/agents/codex.ts Codex Cloud HTTP implementation View source
src/lib/cloudAgent/agents/devin.ts Devin HTTP implementation View source
src/lib/cloudAgent/registry.ts Agent discovery and singleton access View source
src/lib/cloudAgent/types.ts Shared TypeScript interfaces View source
src/lib/cloudAgent/credentials.ts Encrypted API key storage View source
src/lib/cloudAgent/db.ts SQLite task persistence helpers View source
db/migrations/061_cloud_agent_credentials.sql Schema migration for credentials table View source

Summary

  • CloudAgentBase in src/lib/cloudAgent/baseAgent.ts provides the abstract contract that all providers implement
  • Registry pattern via 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 and 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. 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 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, using the schema defined in migration 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, 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. 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →