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:
- ID generation: The agent creates an internal OmniRoute ID (
task_<timestamp>_<random>) viagenerateTaskId()inbaseAgent.ts - Remote creation: The agent calls the provider's API and receives an
externalId - Database write: The full task record is stored in the
cloudAgentTasktable viasrc/lib/cloudAgent/db.ts
The persisted CloudAgentTask contains:
id— OmniRoute's local identifierexternalId— the provider's task identifierproviderId— which agent created the task (codex-cloud,devin,jules, etc.)status— normalized state (queued,running,completed,failed)prompt,source,options— the original input payloadcreatedAt/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.tsprovides the abstract contract that all providers implement - Registry pattern via
registry.tsenables discovery ofjules,devin,codex-cloud, andcursor-cloudagents - 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.tsandagents/devin.tshandle 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →